From 4c017a642ec3145c2101f1939bc714b369e54eaa Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 4 Mar 2024 12:53:46 +0100 Subject: [PATCH 01/40] incorporate match-based filtering into codebase --- src/libs/filterArrayByMatch.ts | 363 +++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 src/libs/filterArrayByMatch.ts diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts new file mode 100644 index 000000000000..c24441945060 --- /dev/null +++ b/src/libs/filterArrayByMatch.ts @@ -0,0 +1,363 @@ +const rankings = { + CASE_SENSITIVE_EQUAL: 7, + EQUAL: 6, + STARTS_WITH: 5, + WORD_STARTS_WITH: 4, + CONTAINS: 3, + ACRONYM: 2, + MATCHES: 1, + NO_MATCH: 0, +} as const; + +const sortType = { + ASC: 'asc', + DESC: 'desc', + NONE: 'none', +} as const; + +type Ranking = (typeof rankings)[keyof typeof rankings]; +type Sort = (typeof sortType)[keyof typeof sortType]; + +type RankingInfo = { + rankedValue: string; + rank: Ranking; + keyIndex: number; + keyThreshold: Ranking | undefined; +}; + +type ValueGetterKey = (item: T) => string | string[]; + +type IndexedItem = { + item: T; + index: number; +}; +type RankedItem = RankingInfo & IndexedItem; + +type KeyAttributesOptions = { + key?: string | ValueGetterKey; + threshold?: Ranking; +}; + +type KeyOption = KeyAttributesOptions | ValueGetterKey | string; + +type Options = { + keys?: ReadonlyArray>; + threshold?: Ranking; + sort?: Sort; + strict?: boolean; +}; +type IndexableByString = Record; + +/** + * Sorts the ranked items based on the sort type + * @param rankedItems list of ranked items + * @param sort sort type + * @returns the sorted list of items + */ +function sortRankedItems(rankedItems: RankedItem[], sort: Sort): T[] { + if (sort === sortType.DESC) { + return rankedItems.sort((a, b) => b.rank - a.rank).map((item) => item.item); + } + + if (sort === sortType.ASC) { + return rankedItems.sort((a, b) => a.rank - b.rank).map((item) => item.item); + } + + return rankedItems.map((item) => item.item); +} + +/** + * Generates an acronym for a string. + * + * @param {String} string the string for which to produce the acronym + * @returns {String} the acronym + */ +function getAcronym(string: string): string { + let acronym = ''; + const wordsInString = string.split(' '); + wordsInString.forEach((wordInString) => { + const splitByHyphenWords = wordInString.split('-'); + splitByHyphenWords.forEach((splitByHyphenWord) => { + acronym += splitByHyphenWord.substring(0, 1); + }); + }); + return acronym; +} + +/** + * Given path: "foo.bar.baz" and item: {foo: {bar: {baz: 'buzz'}}} -> 'buzz' + * @param path a dot-separated set of keys + * @param item the item to get the value from + */ +function getNestedValues(path: string, item: T): string[] { + const keys = path.split('.'); + + type ValueA = Array; + let values: ValueA = [item]; + + for (let i = 0; i < keys.length; i++) { + const nestedKey = keys[i]; + let nestedValues: ValueA = []; + + for (let j = 0; j < values.length; j++) { + const nestedItem = values[j]; + + if (nestedItem == null) continue; + + if (Object.hasOwnProperty.call(nestedItem, nestedKey)) { + const nestedValue = (nestedItem as IndexableByString)[nestedKey]; + if (nestedValue != null) { + nestedValues.push(nestedValue as IndexableByString | string); + } + } else if (nestedKey === '*') { + // ensure that values is an array + nestedValues = nestedValues.concat(nestedItem); + } + } + + values = nestedValues; + } + + if (Array.isArray(values[0])) { + // keep allowing the implicit wildcard for an array of strings at the end of the path + const result: string[] = []; + return result.concat(...(values as string[])); + } + + return values as string[]; +} + +/** + * Gets value for key in item at arbitrarily nested keypath + * @param {Object} item - the item + * @param {Object|Function} key - the potentially nested keypath or property callback + * @return {Array} - an array containing the value(s) at the nested keypath + */ +function getItemValues(item: T, key: KeyOption): string[] { + if (typeof key === 'object') { + key = key.key as string; + } + let value: string | string[] | null | unknown; + if (typeof key === 'function') { + value = key(item); + } else if (item == null) { + value = null; + } else if (Object.hasOwnProperty.call(item, key)) { + value = (item as IndexableByString)[key]; + } else if (key.includes('.')) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + return getNestedValues(key, item); + } else { + value = null; + } + + // because `value` can also be undefined + if (value == null) { + return []; + } + if (Array.isArray(value)) { + return value; + } + return [String(value)]; +} + +/** + * Gets all the values for the given keys in the given item and returns an array of those values + * @param item - the item from which the values will be retrieved + * @param keys - the keys to use to retrieve the values + * @return objects with {itemValue} + */ +function getAllValuesToRank(item: T, keys: ReadonlyArray>) { + const allValues: string[] = []; + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + const itemValues = getItemValues(item, key); + for (let i = 0; i < itemValues.length; i++) { + allValues.push(itemValues[i]); + } + } + return allValues; +} + +/** + * Returns a score based on how spread apart the characters from the stringToRank are within the testString. + * A number close to rankings.MATCHES represents a loose match. A number close to rankings.MATCHES + 1 represents a tighter match. + * @param {String} testString - the string to test against + * @param {String} stringToRank - the string to rank + * @returns {Number} the number between rankings.MATCHES and + */ +function getClosenessRanking(testString: string, stringToRank: string): Ranking { + let matchingInOrderCharCount = 0; + let charNumber = 0; + function findMatchingCharacter(matchChar: string, string: string, index: number) { + for (let j = index; j < string.length; j++) { + if (string[j] === matchChar) { + matchingInOrderCharCount += 1; + return j + 1; + } + } + return -1; + } + + function getRanking(spread: number) { + const spreadPercentage = 1 / spread; + const inOrderPercentage = matchingInOrderCharCount / stringToRank.length; + const ranking = rankings.MATCHES + inOrderPercentage * spreadPercentage; + + return ranking as Ranking; + } + + const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0); + + if (firstIndex < 0) { + return rankings.NO_MATCH; + } + + charNumber = firstIndex; + for (let i = 1; i < stringToRank.length; i++) { + const matchChar = stringToRank[i]; + charNumber = findMatchingCharacter(matchChar, testString, charNumber); + const found = charNumber > -1; + + if (!found) { + return rankings.NO_MATCH; + } + } + + const spread = charNumber - firstIndex; + return getRanking(spread); +} + +/** + * Gives a rankings score based on how well the two strings match. + * @param {String} testString - the string to test against + * @param {String} stringToRank - the string to rank + * @returns {Number} the ranking for how well stringToRank matches testString + */ +function getMatchRanking(testString: string, stringToRank: string): Ranking { + // too long + if (stringToRank.length > testString.length) { + return rankings.NO_MATCH; + } + + // case sensitive equals + if (testString === stringToRank) { + return rankings.CASE_SENSITIVE_EQUAL; + } + + // Lower casing before further comparison + const lowercaseTestString = testString.toLowerCase(); + const lowercaseStringToRank = stringToRank.toLowerCase(); + + // case insensitive equals + if (lowercaseTestString === lowercaseStringToRank) { + return rankings.EQUAL; + } + + // starts with + if (lowercaseTestString.startsWith(lowercaseStringToRank)) { + return rankings.STARTS_WITH; + } + + // word starts with + if (lowercaseTestString.includes(` ${lowercaseStringToRank}`)) { + return rankings.WORD_STARTS_WITH; + } + + // contains + if (lowercaseTestString.includes(lowercaseStringToRank)) { + return rankings.CONTAINS; + } else if (lowercaseStringToRank.length === 1) { + // If the only character in the given stringToRank + // isn't even contained in the testString, then + // it's definitely not a match. + return rankings.NO_MATCH; + } + + // acronym + if (getAcronym(lowercaseTestString).includes(lowercaseStringToRank)) { + return rankings.ACRONYM; + } + + // will return a number between rankings.MATCHES and + // rankings.MATCHES + 1 depending on how close of a match it is. + return getClosenessRanking(lowercaseTestString, lowercaseStringToRank); +} + +/** + * Gets the highest ranking for value for the given item based on its values for the given keys + * @param {*} item - the item to rank + * @param {Array} keys - the keys to get values from the item for the ranking + * @param {String} value - the value to rank against + * @param {Object} options - options to control the ranking + * @return {{rank: Number, keyIndex: Number, keyThreshold: Number}} - the highest ranking + */ +function getHighestRanking(item: T, keys: ReadonlyArray> | undefined, value: string, options: Options): RankingInfo { + if (!keys) { + // if keys is not specified, then we assume the item given is ready to be matched + const stringItem = item as unknown as string; + return { + // ends up being duplicate of 'item' in matches but consistent + rankedValue: stringItem, + rank: rankings.NO_MATCH, // TODO fix it + keyIndex: -1, + keyThreshold: options.threshold, + }; + } + const valuesToRank = getAllValuesToRank(item, keys); + return valuesToRank.reduce( + ({rank, rankedValue, keyIndex, keyThreshold}, itemValue, i) => { + let newRank = getMatchRanking(itemValue, value); + let newRankedValue = rankedValue; + + if (newRank > rank) { + rank = newRank; + keyIndex = i; + keyThreshold = options.threshold; // TODO check if it's correct + newRankedValue = itemValue; + } + return {rankedValue: newRankedValue, rank, keyIndex, keyThreshold}; + }, + { + rankedValue: item as unknown as string, + rank: rankings.NO_MATCH as Ranking, + keyIndex: -1, + keyThreshold: options.threshold, + }, + ); +} + +/** + * Takes an array of items and a value and returns a new array with the items that match the given value + * @param {Array} items - the items to filter + * @param {String} searchValue - the value to use for ranking + * @param {Object} options - options to configure + * @return {Array} - the new sorted array + */ +function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options = {}): T[] { + function reduceItemsToRanked(matches: Array>, item: T, index: number): Array> { + const rankingInfo = getHighestRanking(item, keys, searchValue, options); + const {rank, keyThreshold = threshold} = rankingInfo; + + if (rank >= keyThreshold) { + matches.push({...rankingInfo, item, index}); + } + return matches; + } + + const {keys, threshold = rankings.MATCHES, sort = sortType.DESC, strict = false} = options; + const matchedItems = items.reduce(reduceItemsToRanked, []); + + let itemsToSort = matchedItems; + + if (options.strict) { + itemsToSort = matchedItems.filter((item) => item.rank >= threshold + 1); + } + + return sortRankedItems(itemsToSort, sort); +} + +export default filterArrayByMatch; +export {rankings}; + +export type {Options, KeyAttributesOptions, KeyOption, RankingInfo, ValueGetterKey}; From 3cda73bca89b41bdaf84a328f8c5d629e57a18ae Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 4 Mar 2024 14:23:18 +0100 Subject: [PATCH 02/40] create function responsible for filtering options --- src/libs/OptionsListUtils.ts | 108 +++++++++++++++++++++++++++------ src/libs/filterArrayByMatch.ts | 2 +- 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 79e67164a15a..44f9b4192f92 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -36,6 +36,7 @@ import times from '@src/utils/times'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; +import filterArrayByMatch, {rankings, sortType} from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -149,6 +150,10 @@ type GetOptions = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; +type ReportTypesOptionData = { + [key: string]: ReportUtils.OptionData[]; +}; + /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can * be configured to display different results based on the options passed to the private getOptions() method. Public @@ -1306,6 +1311,29 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function orderOptions(options: ReportUtils.OptionData[], searchValue: string | undefined) { + return lodashOrderBy( + options, + [ + (option) => { + if (!!option.isChatRoom || option.isArchivedRoom) { + return 3; + } + if (!option.login) { + return 2; + } + if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { + return 1; + } + + // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list + return 0; + }, + ], + ['asc'], + ); +} + /** * Build the options */ @@ -1656,26 +1684,7 @@ function getOptions( // When sortByReportTypeInSearch is true, recentReports will be returned with all the reports including personalDetailsOptions in the correct Order. recentReportOptions.push(...personalDetailsOptions); personalDetailsOptions = []; - recentReportOptions = lodashOrderBy( - recentReportOptions, - [ - (option) => { - if (!!option.isChatRoom || option.isArchivedRoom) { - return 3; - } - if (!option.login) { - return 2; - } - if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { - return 1; - } - - // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list - return 0; - }, - ], - ['asc'], - ); + recentReportOptions = orderOptions(recentReportOptions, searchValue); } return { @@ -1997,6 +2006,64 @@ function formatSectionsFromSearchTerm( }; } +function filterOptions(options: GetOptions, searchValue: string): ReportUtils.OptionData[] { + const searchTerms = searchValue.split(' '); + const reportsByType = options.recentReports.reduce( + (acc, option) => { + if (option.isChatRoom) { + acc.chatRooms.push(option); + } else if (option.isPolicyExpenseChat) { + acc.policyExpenseChats.push(option); + } else { + acc.reports.push(option); + } + + return acc; + }, + { + chatRooms: [], + policyExpenseChats: [], + reports: [], + personalDetails: options.personalDetails, + }, + ); + + const createFilter = (items: ReportUtils.OptionData[], keys: string[], term: string) => { + return filterArrayByMatch(items, term, { + keys, + threshold: rankings.MATCHES, + sort: sortType.NONE, + strict: true, + }); + }; + + const matchResults = searchTerms.reduceRight((items, term) => { + const personalDetails = createFilter( + items.personalDetails, + ['text', 'login', 'participantsList[0].displayName', 'participantsList[0].firstName', 'participantsList[0].lastName'], + term, + ); + const chatRooms = createFilter(items.chatRooms, ['text', 'alternateText'], term); + const policyExpenseChats = createFilter(items.policyExpenseChats, ['text'], term); + const reports = createFilter( + items.reports, + ['text', 'participantsList.*.login', 'participantsList.*.displayName', 'participantsList.*.firstName', 'participantsList.*.lastName'], + term, + ); + + return { + personalDetails, + chatRooms, + policyExpenseChats, + reports, + }; + }, reportsByType); + + const filteredOptions = Object.values(matchResults).flat(); + + return orderOptions(filteredOptions, searchValue); +} + export { getAvatarsForAccountIDs, isCurrentUser, @@ -2027,6 +2094,7 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + filterOptions, }; export type {MemberForList, CategorySection, GetOptions}; diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index c24441945060..c9f35824f491 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -358,6 +358,6 @@ function filterArrayByMatch(items: readonly T[], searchValue: string } export default filterArrayByMatch; -export {rankings}; +export {rankings, sortType}; export type {Options, KeyAttributesOptions, KeyOption, RankingInfo, ValueGetterKey}; From c2e3157e8e603662bef2e99e81c91777f2a31960 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 4 Mar 2024 15:09:10 +0100 Subject: [PATCH 03/40] use filtering on search page --- src/libs/OptionsListUtils.ts | 10 +++---- src/pages/SearchPage/index.js | 51 ++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 44f9b4192f92..7d4a6ddf349e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -150,9 +150,7 @@ type GetOptions = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; -type ReportTypesOptionData = { - [key: string]: ReportUtils.OptionData[]; -}; +type ReportTypesOptionData = Record; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -2028,15 +2026,13 @@ function filterOptions(options: GetOptions, searchValue: string): ReportUtils.Op }, ); - const createFilter = (items: ReportUtils.OptionData[], keys: string[], term: string) => { - return filterArrayByMatch(items, term, { + const createFilter = (items: ReportUtils.OptionData[], keys: string[], term: string) => + filterArrayByMatch(items, term, { keys, threshold: rankings.MATCHES, sort: sortType.NONE, strict: true, }); - }; - const matchResults = searchTerms.reduceRight((items, term) => { const personalDetails = createFilter( items.personalDetails, diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 0c17e58837c1..99dd26266e12 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -54,6 +54,12 @@ function SearchPage({betas, reports, isSearchingForReports}) { const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); + const [options, setOptions] = useState({ + recentReports: [], + personalDetails: [], + betas: [], + }); + const [filteredOptions, setFilteredOptions] = useState([]); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -68,6 +74,28 @@ function SearchPage({betas, reports, isSearchingForReports}) { Report.searchInServer(debouncedSearchValue.trim()); }, [debouncedSearchValue]); + useEffect(() => { + if (!isScreenTransitionEnd) { + return; + } + + const searchOptions = OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas); + setOptions(searchOptions); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isScreenTransitionEnd]); + + useEffect(() => { + if (debouncedSearchValue.trim() === '') { + setFilteredOptions({}); + + return; + } + + const filteredResults = OptionsListUtils.filterOptions(options, debouncedSearchValue); + setFilteredOptions(filteredResults); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedSearchValue]); + const { recentReports, personalDetails: localPersonalDetails, @@ -76,16 +104,27 @@ function SearchPage({betas, reports, isSearchingForReports}) { } = useMemo(() => { if (!isScreenTransitionEnd) { return { - recentReports: {}, - personalDetails: {}, + recentReports: [], + personalDetails: [], userToInvite: {}, headerMessage: '', }; } - const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas); - const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); - return {...options, headerMessage: header}; - }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + + let listOptions = options; + let header = OptionsListUtils.getHeaderMessage(listOptions.recentReports.length + listOptions.personalDetails.length !== 0, Boolean(listOptions.userToInvite), debouncedSearchValue); + + if (searchValue !== '' && filteredOptions.length > 0) { + listOptions = { + recentReports: filteredOptions, + personalDetails: [], + userToInvite: {}, + }; + header = ''; + } + + return {...listOptions, headerMessage: header}; + }, [debouncedSearchValue, filteredOptions, isScreenTransitionEnd, options, searchValue]); const sections = useMemo(() => { const newSections = []; From bd323a045b3bcc4425e37d8fbc1de968f60cb877 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 5 Mar 2024 09:25:52 +0100 Subject: [PATCH 04/40] adapt alghoritm file to match project standards --- src/libs/filterArrayByMatch.ts | 140 +++++++++++++++------------------ 1 file changed, 65 insertions(+), 75 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index c9f35824f491..1b6c002dfcfd 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -54,7 +54,7 @@ type IndexableByString = Record; * @param sort sort type * @returns the sorted list of items */ -function sortRankedItems(rankedItems: RankedItem[], sort: Sort): T[] { +function sortRankedItems(rankedItems: Array>, sort: Sort): T[] { if (sort === sortType.DESC) { return rankedItems.sort((a, b) => b.rank - a.rank).map((item) => item.item); } @@ -68,9 +68,8 @@ function sortRankedItems(rankedItems: RankedItem[], sort: Sort): T[] { /** * Generates an acronym for a string. - * - * @param {String} string the string for which to produce the acronym - * @returns {String} the acronym + * @param string the string for which to produce the acronym + * @returns the acronym */ function getAcronym(string: string): string { let acronym = ''; @@ -91,27 +90,21 @@ function getAcronym(string: string): string { */ function getNestedValues(path: string, item: T): string[] { const keys = path.split('.'); - - type ValueA = Array; - let values: ValueA = [item]; - - for (let i = 0; i < keys.length; i++) { - const nestedKey = keys[i]; - let nestedValues: ValueA = []; - - for (let j = 0; j < values.length; j++) { - const nestedItem = values[j]; - - if (nestedItem == null) continue; - - if (Object.hasOwnProperty.call(nestedItem, nestedKey)) { - const nestedValue = (nestedItem as IndexableByString)[nestedKey]; - if (nestedValue != null) { - nestedValues.push(nestedValue as IndexableByString | string); + let values: Array = [item]; + + for (const nestedKey of keys) { + let nestedValues: Array = []; + + for (const nestedItem of values) { + if (nestedItem != null) { + if (Object.hasOwnProperty.call(nestedItem, nestedKey)) { + const nestedValue = (nestedItem as IndexableByString)[nestedKey]; + if (nestedValue != null) { + nestedValues.push(nestedValue as IndexableByString | string); + } + } else if (nestedKey === '*') { + nestedValues = nestedValues.concat(nestedItem); } - } else if (nestedKey === '*') { - // ensure that values is an array - nestedValues = nestedValues.concat(nestedItem); } } @@ -119,7 +112,6 @@ function getNestedValues(path: string, item: T): string[] { } if (Array.isArray(values[0])) { - // keep allowing the implicit wildcard for an array of strings at the end of the path const result: string[] = []; return result.concat(...(values as string[])); } @@ -129,24 +121,25 @@ function getNestedValues(path: string, item: T): string[] { /** * Gets value for key in item at arbitrarily nested keypath - * @param {Object} item - the item - * @param {Object|Function} key - the potentially nested keypath or property callback - * @return {Array} - an array containing the value(s) at the nested keypath + * @param item - the item + * @param key - the potentially nested keypath or property callback + * @returns an array containing the value(s) at the nested keypath */ function getItemValues(item: T, key: KeyOption): string[] { - if (typeof key === 'object') { - key = key.key as string; + let keyCopy = key; + if (typeof keyCopy === 'object') { + keyCopy = keyCopy.key as string; } + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents let value: string | string[] | null | unknown; - if (typeof key === 'function') { - value = key(item); + if (typeof keyCopy === 'function') { + value = keyCopy(item); } else if (item == null) { value = null; - } else if (Object.hasOwnProperty.call(item, key)) { - value = (item as IndexableByString)[key]; - } else if (key.includes('.')) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return getNestedValues(key, item); + } else if (Object.hasOwnProperty.call(item, keyCopy)) { + value = (item as IndexableByString)[keyCopy]; + } else if (keyCopy.includes('.')) { + return getNestedValues(keyCopy, item); } else { value = null; } @@ -156,7 +149,7 @@ function getItemValues(item: T, key: KeyOption): string[] { return []; } if (Array.isArray(value)) { - return value; + return value as string[]; } return [String(value)]; } @@ -169,11 +162,10 @@ function getItemValues(item: T, key: KeyOption): string[] { */ function getAllValuesToRank(item: T, keys: ReadonlyArray>) { const allValues: string[] = []; - for (let j = 0; j < keys.length; j++) { - const key = keys[j]; + for (const key of keys) { const itemValues = getItemValues(item, key); - for (let i = 0; i < itemValues.length; i++) { - allValues.push(itemValues[i]); + for (const value of itemValues) { + allValues.push(value); } } return allValues; @@ -182,9 +174,9 @@ function getAllValuesToRank(item: T, keys: ReadonlyArray>) { /** * Returns a score based on how spread apart the characters from the stringToRank are within the testString. * A number close to rankings.MATCHES represents a loose match. A number close to rankings.MATCHES + 1 represents a tighter match. - * @param {String} testString - the string to test against - * @param {String} stringToRank - the string to rank - * @returns {Number} the number between rankings.MATCHES and + * @param testString - the string to test against + * @param stringToRank - the string to rank + * @returns the number between rankings.MATCHES and */ function getClosenessRanking(testString: string, stringToRank: string): Ranking { let matchingInOrderCharCount = 0; @@ -230,9 +222,9 @@ function getClosenessRanking(testString: string, stringToRank: string): Ranking /** * Gives a rankings score based on how well the two strings match. - * @param {String} testString - the string to test against - * @param {String} stringToRank - the string to rank - * @returns {Number} the ranking for how well stringToRank matches testString + * @param testString - the string to test against + * @param stringToRank - the string to rank + * @returns the ranking for how well stringToRank matches testString */ function getMatchRanking(testString: string, stringToRank: string): Ranking { // too long @@ -267,10 +259,8 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { // contains if (lowercaseTestString.includes(lowercaseStringToRank)) { return rankings.CONTAINS; - } else if (lowercaseStringToRank.length === 1) { - // If the only character in the given stringToRank - // isn't even contained in the testString, then - // it's definitely not a match. + } + if (lowercaseStringToRank.length === 1) { return rankings.NO_MATCH; } @@ -279,18 +269,17 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { return rankings.ACRONYM; } - // will return a number between rankings.MATCHES and - // rankings.MATCHES + 1 depending on how close of a match it is. + // will return a number between rankings.MATCHES and rankings.MATCHES + 1 depending on how close of a match it is. return getClosenessRanking(lowercaseTestString, lowercaseStringToRank); } /** * Gets the highest ranking for value for the given item based on its values for the given keys - * @param {*} item - the item to rank - * @param {Array} keys - the keys to get values from the item for the ranking - * @param {String} value - the value to rank against - * @param {Object} options - options to control the ranking - * @return {{rank: Number, keyIndex: Number, keyThreshold: Number}} - the highest ranking + * @param item - the item to rank + * @param keys - the keys to get values from the item for the ranking + * @param value - the value to rank against + * @param options - options to control the ranking + * @returns the highest ranking */ function getHighestRanking(item: T, keys: ReadonlyArray> | undefined, value: string, options: Options): RankingInfo { if (!keys) { @@ -306,17 +295,18 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef } const valuesToRank = getAllValuesToRank(item, keys); return valuesToRank.reduce( - ({rank, rankedValue, keyIndex, keyThreshold}, itemValue, i) => { - let newRank = getMatchRanking(itemValue, value); - let newRankedValue = rankedValue; - - if (newRank > rank) { - rank = newRank; - keyIndex = i; - keyThreshold = options.threshold; // TODO check if it's correct + (acc, itemValue, index) => { + const ranking = acc; + const newRank = getMatchRanking(itemValue, value); + let newRankedValue = ranking.rankedValue; + + if (newRank > ranking.rank) { + ranking.rank = newRank; + ranking.keyIndex = index; + ranking.keyThreshold = options.threshold; // TODO check if it's correct newRankedValue = itemValue; } - return {rankedValue: newRankedValue, rank, keyIndex, keyThreshold}; + return {rankedValue: newRankedValue, rank: ranking.rank, keyIndex: ranking.keyIndex, keyThreshold: ranking.keyThreshold}; }, { rankedValue: item as unknown as string, @@ -329,12 +319,14 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef /** * Takes an array of items and a value and returns a new array with the items that match the given value - * @param {Array} items - the items to filter - * @param {String} searchValue - the value to use for ranking - * @param {Object} options - options to configure - * @return {Array} - the new sorted array + * @param items - the items to filter + * @param searchValue - the value to use for ranking + * @param options - options to configure + * @returns the new sorted array */ function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options = {}): T[] { + const {keys, threshold = rankings.MATCHES, sort = sortType.DESC, strict = false} = options; + function reduceItemsToRanked(matches: Array>, item: T, index: number): Array> { const rankingInfo = getHighestRanking(item, keys, searchValue, options); const {rank, keyThreshold = threshold} = rankingInfo; @@ -345,12 +337,10 @@ function filterArrayByMatch(items: readonly T[], searchValue: string return matches; } - const {keys, threshold = rankings.MATCHES, sort = sortType.DESC, strict = false} = options; const matchedItems = items.reduce(reduceItemsToRanked, []); - let itemsToSort = matchedItems; - if (options.strict) { + if (strict) { itemsToSort = matchedItems.filter((item) => item.rank >= threshold + 1); } From 4a4ba241e7f26da929b96dee333e8d1bb95af4b1 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 6 Mar 2024 14:46:01 +0100 Subject: [PATCH 05/40] update filtering method & refactor options filtering --- src/libs/OptionsListUtils.ts | 25 ++++++++++--------------- src/libs/filterArrayByMatch.ts | 12 ++++++------ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7d4a6ddf349e..248a92fe84e8 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -36,7 +36,7 @@ import times from '@src/utils/times'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; -import filterArrayByMatch, {rankings, sortType} from './filterArrayByMatch'; +import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -432,6 +432,7 @@ function getSearchText( } } } + if (report) { Array.prototype.push.apply(searchTerms, reportName.split(/[,\s]/)); @@ -2006,12 +2007,11 @@ function formatSectionsFromSearchTerm( function filterOptions(options: GetOptions, searchValue: string): ReportUtils.OptionData[] { const searchTerms = searchValue.split(' '); + const reportsByType = options.recentReports.reduce( (acc, option) => { - if (option.isChatRoom) { - acc.chatRooms.push(option); - } else if (option.isPolicyExpenseChat) { - acc.policyExpenseChats.push(option); + if (option.isChatRoom || option.isPolicyExpenseChat) { + acc.chatRoomsAndPolicyExpenseChats.push(option); } else { acc.reports.push(option); } @@ -2019,28 +2019,24 @@ function filterOptions(options: GetOptions, searchValue: string): ReportUtils.Op return acc; }, { - chatRooms: [], - policyExpenseChats: [], + chatRoomsAndPolicyExpenseChats: [], reports: [], personalDetails: options.personalDetails, }, ); - const createFilter = (items: ReportUtils.OptionData[], keys: string[], term: string) => filterArrayByMatch(items, term, { keys, - threshold: rankings.MATCHES, - sort: sortType.NONE, strict: true, }); + const matchResults = searchTerms.reduceRight((items, term) => { const personalDetails = createFilter( items.personalDetails, ['text', 'login', 'participantsList[0].displayName', 'participantsList[0].firstName', 'participantsList[0].lastName'], term, ); - const chatRooms = createFilter(items.chatRooms, ['text', 'alternateText'], term); - const policyExpenseChats = createFilter(items.policyExpenseChats, ['text'], term); + const chatRoomsAndPolicyExpenseChats = createFilter(items.chatRoomsAndPolicyExpenseChats, ['text', 'alternateText'], term); const reports = createFilter( items.reports, ['text', 'participantsList.*.login', 'participantsList.*.displayName', 'participantsList.*.firstName', 'participantsList.*.lastName'], @@ -2048,10 +2044,9 @@ function filterOptions(options: GetOptions, searchValue: string): ReportUtils.Op ); return { - personalDetails, - chatRooms, - policyExpenseChats, reports, + personalDetails, + chatRoomsAndPolicyExpenseChats, }; }, reportsByType); diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index 1b6c002dfcfd..647e7a1c3edf 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -54,16 +54,16 @@ type IndexableByString = Record; * @param sort sort type * @returns the sorted list of items */ -function sortRankedItems(rankedItems: Array>, sort: Sort): T[] { +function sortRankedItems(rankedItems: Array>, sort?: Sort): Array> { if (sort === sortType.DESC) { - return rankedItems.sort((a, b) => b.rank - a.rank).map((item) => item.item); + return rankedItems.sort((a, b) => b.rank - a.rank); } if (sort === sortType.ASC) { - return rankedItems.sort((a, b) => a.rank - b.rank).map((item) => item.item); + return rankedItems.sort((a, b) => a.rank - b.rank); } - return rankedItems.map((item) => item.item); + return rankedItems; } /** @@ -344,10 +344,10 @@ function filterArrayByMatch(items: readonly T[], searchValue: string itemsToSort = matchedItems.filter((item) => item.rank >= threshold + 1); } - return sortRankedItems(itemsToSort, sort); + return sortRankedItems(itemsToSort, sort).map((item) => item.item); } export default filterArrayByMatch; -export {rankings, sortType}; +export {rankings}; export type {Options, KeyAttributesOptions, KeyOption, RankingInfo, ValueGetterKey}; From 74a2ecda51a9ea7714ed2119ff7e392dda7c6156 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 6 Mar 2024 14:52:17 +0100 Subject: [PATCH 06/40] remove comments --- src/libs/filterArrayByMatch.ts | 4 ++-- src/pages/SearchPage/index.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index 647e7a1c3edf..abd7a2b773c8 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -288,7 +288,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef return { // ends up being duplicate of 'item' in matches but consistent rankedValue: stringItem, - rank: rankings.NO_MATCH, // TODO fix it + rank: rankings.NO_MATCH, keyIndex: -1, keyThreshold: options.threshold, }; @@ -303,7 +303,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef if (newRank > ranking.rank) { ranking.rank = newRank; ranking.keyIndex = index; - ranking.keyThreshold = options.threshold; // TODO check if it's correct + ranking.keyThreshold = options.threshold; newRankedValue = itemValue; } return {rankedValue: newRankedValue, rank: ranking.rank, keyIndex: ranking.keyIndex, keyThreshold: ranking.keyThreshold}; diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 99dd26266e12..b34d18b2c618 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -147,8 +148,7 @@ function SearchPage({betas, reports, isSearchingForReports}) { }); indexOffset += recentReports.length; } - - if (userToInvite) { + if (!_.isEmpty(userToInvite)) { newSections.push({ data: [userToInvite], shouldShow: true, From d2b234cbd5547e122738adfba0b12c7f0910e4b8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 6 Mar 2024 15:52:44 +0100 Subject: [PATCH 07/40] update search page to filter options --- src/libs/OptionsListUtils.ts | 8 ++++++-- src/pages/SearchPage/index.js | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 248a92fe84e8..93e2f1b01a1f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -432,7 +432,6 @@ function getSearchText( } } } - if (report) { Array.prototype.push.apply(searchTerms, reportName.split(/[,\s]/)); @@ -1310,6 +1309,12 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +/** + * Options need to be sorted in the specific order + * @param options - list of options to be sorted + * @param searchValue - search string + * @returns a sorted list of options + */ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | undefined) { return lodashOrderBy( options, @@ -2051,7 +2056,6 @@ function filterOptions(options: GetOptions, searchValue: string): ReportUtils.Op }, reportsByType); const filteredOptions = Object.values(matchResults).flat(); - return orderOptions(filteredOptions, searchValue); } diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index b34d18b2c618..43c0feddda42 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -113,7 +113,6 @@ function SearchPage({betas, reports, isSearchingForReports}) { } let listOptions = options; - let header = OptionsListUtils.getHeaderMessage(listOptions.recentReports.length + listOptions.personalDetails.length !== 0, Boolean(listOptions.userToInvite), debouncedSearchValue); if (searchValue !== '' && filteredOptions.length > 0) { listOptions = { @@ -122,8 +121,14 @@ function SearchPage({betas, reports, isSearchingForReports}) { userToInvite: {}, }; header = ''; + } else if (searchValue !== '' && filteredOptions.length === 0) { + listOptions = { + recentReports: [], + personalDetails: [], + userToInvite: null, + }; } - + let header = OptionsListUtils.getHeaderMessage(listOptions.recentReports.length + listOptions.personalDetails.length !== 0, Boolean(listOptions.userToInvite), debouncedSearchValue); return {...listOptions, headerMessage: header}; }, [debouncedSearchValue, filteredOptions, isScreenTransitionEnd, options, searchValue]); From 39b5036ae0b035c4ab4e1a8841d308aab42a7dc9 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 7 Mar 2024 13:13:05 +0100 Subject: [PATCH 08/40] add regex email to filtering results --- src/libs/OptionsListUtils.ts | 27 +++++++++++++++++++++++---- src/pages/SearchPage/index.js | 8 ++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 93e2f1b01a1f..4da3e097629b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -37,6 +37,7 @@ import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import filterArrayByMatch from './filterArrayByMatch'; +import type {KeyOption} from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -2015,7 +2016,7 @@ function filterOptions(options: GetOptions, searchValue: string): ReportUtils.Op const reportsByType = options.recentReports.reduce( (acc, option) => { - if (option.isChatRoom || option.isPolicyExpenseChat) { + if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { acc.chatRoomsAndPolicyExpenseChats.push(option); } else { acc.reports.push(option); @@ -2029,22 +2030,40 @@ function filterOptions(options: GetOptions, searchValue: string): ReportUtils.Op personalDetails: options.personalDetails, }, ); - const createFilter = (items: ReportUtils.OptionData[], keys: string[], term: string) => + const createFilter = (items: ReportUtils.OptionData[], keys: ReadonlyArray>, term: string) => filterArrayByMatch(items, term, { keys, strict: true, }); + // The regex below is used to remove dots only from the local part of the user email (local-part@domain) + // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) + const emailRegex = /\.(?=[^\s@]*@)/g; + const matchResults = searchTerms.reduceRight((items, term) => { const personalDetails = createFilter( items.personalDetails, - ['text', 'login', 'participantsList[0].displayName', 'participantsList[0].firstName', 'participantsList[0].lastName'], + [ + 'text', + 'login', + (item) => (item.login ? item.login.replace(emailRegex, '') : []), + 'participantsList.0.displayName', + 'participantsList.0.firstName', + 'participantsList.0.lastName', + ], term, ); const chatRoomsAndPolicyExpenseChats = createFilter(items.chatRoomsAndPolicyExpenseChats, ['text', 'alternateText'], term); const reports = createFilter( items.reports, - ['text', 'participantsList.*.login', 'participantsList.*.displayName', 'participantsList.*.firstName', 'participantsList.*.lastName'], + [ + 'text', + 'participantsList.0.login', + (item) => (item.participantsList ? item.participantsList.filter(({login}) => login).map((i) => (i.login ? i.login.replace(emailRegex, '') : '')) : []), + 'participantsList.0.displayName', + 'participantsList.0.firstName', + 'participantsList.0.lastName', + ], term, ); diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 43c0feddda42..49fe0e6dd51e 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -119,8 +119,8 @@ function SearchPage({betas, reports, isSearchingForReports}) { recentReports: filteredOptions, personalDetails: [], userToInvite: {}, + headerMessage: '', }; - header = ''; } else if (searchValue !== '' && filteredOptions.length === 0) { listOptions = { recentReports: [], @@ -128,7 +128,11 @@ function SearchPage({betas, reports, isSearchingForReports}) { userToInvite: null, }; } - let header = OptionsListUtils.getHeaderMessage(listOptions.recentReports.length + listOptions.personalDetails.length !== 0, Boolean(listOptions.userToInvite), debouncedSearchValue); + const header = OptionsListUtils.getHeaderMessage( + listOptions.recentReports.length + listOptions.personalDetails.length !== 0, + Boolean(listOptions.userToInvite), + debouncedSearchValue, + ); return {...listOptions, headerMessage: header}; }, [debouncedSearchValue, filteredOptions, isScreenTransitionEnd, options, searchValue]); From b04b4ae1306752858361a3ba5277b177f52a493b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 7 Mar 2024 18:00:22 +0100 Subject: [PATCH 09/40] remove sorting --- src/libs/filterArrayByMatch.ts | 37 +++++----------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index abd7a2b773c8..dd2ff6c0f239 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -9,14 +9,7 @@ const rankings = { NO_MATCH: 0, } as const; -const sortType = { - ASC: 'asc', - DESC: 'desc', - NONE: 'none', -} as const; - type Ranking = (typeof rankings)[keyof typeof rankings]; -type Sort = (typeof sortType)[keyof typeof sortType]; type RankingInfo = { rankedValue: string; @@ -43,29 +36,10 @@ type KeyOption = KeyAttributesOptions | ValueGetterKey | string; type Options = { keys?: ReadonlyArray>; threshold?: Ranking; - sort?: Sort; strict?: boolean; }; type IndexableByString = Record; -/** - * Sorts the ranked items based on the sort type - * @param rankedItems list of ranked items - * @param sort sort type - * @returns the sorted list of items - */ -function sortRankedItems(rankedItems: Array>, sort?: Sort): Array> { - if (sort === sortType.DESC) { - return rankedItems.sort((a, b) => b.rank - a.rank); - } - - if (sort === sortType.ASC) { - return rankedItems.sort((a, b) => a.rank - b.rank); - } - - return rankedItems; -} - /** * Generates an acronym for a string. * @param string the string for which to produce the acronym @@ -322,10 +296,10 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef * @param items - the items to filter * @param searchValue - the value to use for ranking * @param options - options to configure - * @returns the new sorted array + * @returns the new filtered array */ function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options = {}): T[] { - const {keys, threshold = rankings.MATCHES, sort = sortType.DESC, strict = false} = options; + const {keys, threshold = rankings.MATCHES, strict = false} = options; function reduceItemsToRanked(matches: Array>, item: T, index: number): Array> { const rankingInfo = getHighestRanking(item, keys, searchValue, options); @@ -337,14 +311,13 @@ function filterArrayByMatch(items: readonly T[], searchValue: string return matches; } - const matchedItems = items.reduce(reduceItemsToRanked, []); - let itemsToSort = matchedItems; + let matchedItems = items.reduce(reduceItemsToRanked, []); if (strict) { - itemsToSort = matchedItems.filter((item) => item.rank >= threshold + 1); + matchedItems = matchedItems.filter((item) => item.rank >= threshold + 1); } - return sortRankedItems(itemsToSort, sort).map((item) => item.item); + return matchedItems.map((item) => item.item); } export default filterArrayByMatch; From 67e5dc8fd95a187cfe9a1deeeff6edf0660703d8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 8 Mar 2024 12:37:22 +0100 Subject: [PATCH 10/40] update filter by match --- src/libs/filterArrayByMatch.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index dd2ff6c0f239..c8b7eb617dfc 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -1,3 +1,8 @@ +/** + * This file is a slim version of match-sorter library (https://github.com/kentcdodds/match-sorter) adjusted to the needs. + Use `threshold` option with one of the rankings defined below to control the strictness of the match. +*/ + const rankings = { CASE_SENSITIVE_EQUAL: 7, EQUAL: 6, @@ -115,13 +120,9 @@ function getItemValues(item: T, key: KeyOption): string[] { } else if (keyCopy.includes('.')) { return getNestedValues(keyCopy, item); } else { - value = null; - } - - // because `value` can also be undefined - if (value == null) { return []; } + if (Array.isArray(value)) { return value as string[]; } From 7c2c0778948326693f3243b5f17785d02fc1c687 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 8 Mar 2024 14:58:51 +0100 Subject: [PATCH 11/40] update filtered options state --- src/pages/SearchPage/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 49fe0e6dd51e..d3168be07ac4 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -60,7 +60,7 @@ function SearchPage({betas, reports, isSearchingForReports}) { personalDetails: [], betas: [], }); - const [filteredOptions, setFilteredOptions] = useState([]); + const [filteredOptions, setFilteredOptions] = useState({}); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -87,7 +87,9 @@ function SearchPage({betas, reports, isSearchingForReports}) { useEffect(() => { if (debouncedSearchValue.trim() === '') { - setFilteredOptions({}); + if(!_.isEmpty(filteredOptions)) { + setFilteredOptions({}); + } return; } From 450c5f01e9e9e44b78fbbcb155edd1b8565dba18 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 8 Mar 2024 16:03:49 +0100 Subject: [PATCH 12/40] reduce amount of rerenders --- src/pages/SearchPage/index.js | 90 +++++++++++++---------------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index c89b87d0ceef..06f4c193d823 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -63,12 +63,6 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); - const [options, setOptions] = useState({ - recentReports: [], - personalDetails: [], - betas: [], - }); - const [filteredOptions, setFilteredOptions] = useState({}); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -83,69 +77,48 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { Report.searchInServer(debouncedSearchValue.trim()); }, [debouncedSearchValue]); - useEffect(() => { - if (!isScreenTransitionEnd) { - return; - } - - const searchOptions = OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas); - setOptions(searchOptions); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isScreenTransitionEnd]); - - useEffect(() => { - if (debouncedSearchValue.trim() === '') { - if(!_.isEmpty(filteredOptions)) { - setFilteredOptions({}); - } - - return; - } - - const filteredResults = OptionsListUtils.filterOptions(options, debouncedSearchValue); - setFilteredOptions(filteredResults); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchValue]); - - const { - recentReports, - personalDetails: localPersonalDetails, - userToInvite, - headerMessage, - } = useMemo(() => { + const searchOptions = useMemo(() => { if (!isScreenTransitionEnd) { return { recentReports: [], personalDetails: [], - userToInvite: {}, + userToInvite: null, headerMessage: '', }; } + const options = OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas); + const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + return {...options, headerMessage: header}; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isScreenTransitionEnd]); - let listOptions = options; - - if (searchValue !== '' && filteredOptions.length > 0) { - listOptions = { - recentReports: filteredOptions, - personalDetails: [], - userToInvite: {}, - headerMessage: '', - }; - } else if (searchValue !== '' && filteredOptions.length === 0) { - listOptions = { + const filteredOptions = useMemo(() => { + if(debouncedSearchValue.trim() === '') { + return { recentReports: [], personalDetails: [], userToInvite: null, - }; + headerMessage: '' + } + } + + const options = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue); + const header = OptionsListUtils.getHeaderMessage(options.length, false, debouncedSearchValue); + return { + recentReports: options, + personalDetails: [], + userToInvite: null, + headerMessage: header } - const header = OptionsListUtils.getHeaderMessage( - listOptions.recentReports.length + listOptions.personalDetails.length !== 0, - Boolean(listOptions.userToInvite), - debouncedSearchValue, - ); - return {...listOptions, headerMessage: header}; - }, [debouncedSearchValue, filteredOptions, isScreenTransitionEnd, options, searchValue]); + }, [searchOptions, debouncedSearchValue]); + const { + recentReports, + personalDetails: localPersonalDetails, + userToInvite, + headerMessage, + } = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; + const sections = useMemo(() => { const newSections = []; let indexOffset = 0; @@ -167,6 +140,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { }); indexOffset += recentReports.length; } + if (!_.isEmpty(userToInvite)) { newSections.push({ data: [userToInvite], @@ -196,7 +170,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { }; const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - + return ( Date: Mon, 11 Mar 2024 09:07:06 +0100 Subject: [PATCH 13/40] update linting --- src/pages/SearchPage/index.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 06f4c193d823..9ce24b38ac91 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -89,36 +89,31 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { const options = OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas); const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); return {...options, headerMessage: header}; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isScreenTransitionEnd]); const filteredOptions = useMemo(() => { - if(debouncedSearchValue.trim() === '') { + if (debouncedSearchValue.trim() === '') { return { recentReports: [], personalDetails: [], userToInvite: null, - headerMessage: '' - } + headerMessage: '', + }; } - + const options = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue); const header = OptionsListUtils.getHeaderMessage(options.length, false, debouncedSearchValue); return { recentReports: options, personalDetails: [], userToInvite: null, - headerMessage: header - } + headerMessage: header, + }; }, [searchOptions, debouncedSearchValue]); - const { - recentReports, - personalDetails: localPersonalDetails, - userToInvite, - headerMessage, - } = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; - + const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; + const sections = useMemo(() => { const newSections = []; let indexOffset = 0; @@ -170,7 +165,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}) { }; const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - + return ( Date: Tue, 12 Mar 2024 09:01:56 +0100 Subject: [PATCH 14/40] filter by phone number --- src/libs/OptionsListUtils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ba9a6c469a5b..03893aaddcbf 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2046,8 +2046,10 @@ function formatSectionsFromSearchTerm( }; } -function filterOptions(options: Partial, searchValue: string): ReportUtils.OptionData[] { - const searchTerms = searchValue.split(' '); +function filterOptions(options: Partial, searchInputValue: string): ReportUtils.OptionData[] { + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const searchTerms = searchValue ? searchValue.split(' ') : []; const reportsByType = (options.recentReports ?? []).reduce( (acc, option) => { From d9d0e84634f1001aeaae065abe0113aca48f97fe Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 10:09:06 +0100 Subject: [PATCH 15/40] update search options header --- src/pages/SearchPage/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 25e6100c0b6e..f0b982b3d4ce 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -85,10 +85,9 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP }; } const options = OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas ?? []); - const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), ''); return {...options, headerMessage: header}; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isScreenTransitionEnd]); + }, [betas, isScreenTransitionEnd, personalDetails, reports]); const filteredOptions = useMemo(() => { if (debouncedSearchValue.trim() === '') { From 32a71007c05442d28a4ece08b6d0d6ac7b884ec7 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 10:10:37 +0100 Subject: [PATCH 16/40] revert package lock --- package-lock.json | 655 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 611 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5cf96414d5f1..81686286bf1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.50-5", + "version": "1.4.49-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.50-5", + "version": "1.4.49-4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -64,7 +64,7 @@ "lodash": "4.17.21", "lottie-react-native": "6.4.1", "mapbox-gl": "^2.15.0", - "onfido-sdk-ui": "14.15.0", + "onfido-sdk-ui": "13.6.1", "patch-package": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", @@ -107,11 +107,9 @@ "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", "react-native-reanimated": "^3.7.2", - "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", - "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", "react-native-tab-view": "^3.5.2", @@ -229,6 +227,7 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", + "react-native-performance-flipper-reporter": "^2.0.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", "setimmediate": "^1.0.5", @@ -7714,6 +7713,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mediapipe/face_detection": { + "version": "0.4.1646425229", + "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", + "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==" + }, + "node_modules/@mediapipe/face_mesh": { + "version": "0.4.1633559619", + "resolved": "https://registry.npmjs.org/@mediapipe/face_mesh/-/face_mesh-0.4.1633559619.tgz", + "integrity": "sha512-Vc8cdjxS5+O2gnjWH9KncYpUCVXT0h714KlWAsyqJvJbIgUJBqpppbIx8yWcAzBDxm/5cYSuBI5p5ySIPxzcEg==" + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -8045,6 +8054,153 @@ "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", "license": "MIT" }, + "node_modules/@onfido/active-video-capture": { + "version": "0.28.6", + "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", + "integrity": "sha512-RFUeKaOSjj/amPp6VzhVkq/7kIkutEnnttT9n5KDeD3Vx8a09KD3a/xvxdQppveHlDAYsdBP6LrJwSSpjXiprg==", + "dependencies": { + "@mediapipe/face_detection": "^0.4.1646425229", + "@mediapipe/face_mesh": "^0.4.1633559619", + "@onfido/castor": "^2.2.2", + "@onfido/castor-icons": "^2.12.0", + "@tensorflow-models/face-detection": "^1.0.1", + "@tensorflow-models/face-landmarks-detection": "^1.0.2", + "@tensorflow/tfjs-backend-wasm": "3.20.0", + "@tensorflow/tfjs-backend-webgl": "3.20.0", + "@tensorflow/tfjs-converter": "3.20.0", + "@tensorflow/tfjs-core": "3.20.0", + "preact": "10.11.3", + "react-webcam": "^7.2.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@tensorflow-models/face-landmarks-detection": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tensorflow-models/face-landmarks-detection/-/face-landmarks-detection-1.0.5.tgz", + "integrity": "sha512-54XJPi8g29/MknJ33ZBrLsEzr9kw/dJtrJMMD3xrCrnRlfFQPIKQ5PI2Wml55Fz2p4U2hemzBB0/H+S94JddIQ==", + "dependencies": { + "rimraf": "^3.0.2" + }, + "peerDependencies": { + "@mediapipe/face_mesh": "~0.4.0", + "@tensorflow-models/face-detection": "~1.0.0", + "@tensorflow/tfjs-backend-webgl": "^3.12.0", + "@tensorflow/tfjs-converter": "^3.12.0", + "@tensorflow/tfjs-core": "^3.12.0" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.20.0.tgz", + "integrity": "sha512-gf075YaBLwSAAiUwa0D4GvYyUBhbJ1BVSivUNQmUfGKvIr2lIhF0qstBr033YTc3lhkbFSHEEPAHh/EfpqyjXQ==", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.20.0" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-wasm": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-wasm/-/tfjs-backend-wasm-3.20.0.tgz", + "integrity": "sha512-k+sDcrcPtGToLjKRffgtSqlcN4MC6g4hXWRarZfgvvyvFqpxVfVqrGYHGTirXdN47sKYhmcTSMvbM2quGaaQnA==", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "3.20.0", + "@types/emscripten": "~0.0.34" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.20.0" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.20.0.tgz", + "integrity": "sha512-SucbyQ08re3HvRgVfarRtKFIjNM4JvIAzcXmw4vaE/HrCtPEePkGO1VrmfQoN470EdUmGiwgqAjoyBvM2VOlVg==", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "3.20.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "@types/webgl-ext": "0.0.30", + "@types/webgl2": "0.0.6", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.20.0" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-converter": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.20.0.tgz", + "integrity": "sha512-8EIYqtQwvSYw9GFNW2OFU8Qnl/FQF/kKAsQJoORYaZ419WJo+FIZWbAWDtCpJSAgkgoHH1jYWgV9H313cVmqxg==", + "peerDependencies": { + "@tensorflow/tfjs-core": "3.20.0" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-core": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.20.0.tgz", + "integrity": "sha512-L16JyVA4a8jFJXFgB9/oYZxcGq/GfLypt5dMVTyedznARZZ9SiY/UMMbo3IKl9ZylG1dOVVTpjzV3EvBYfeJXw==", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "@types/webgl-ext": "0.0.30", + "@webgpu/types": "0.1.16", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@onfido/active-video-capture/node_modules/@webgpu/types": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", + "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==" + }, + "node_modules/@onfido/castor": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@onfido/castor/-/castor-2.3.0.tgz", + "integrity": "sha512-FkydkjedS6b2g3SqgZMYnVRZvUs/MkaEuXXJWG9+LNc7DMFT1K8smOnNuHzkiM3cJhXL6yAADdKE0mg+ZIrucQ==", + "dependencies": { + "@onfido/castor-tokens": "^1.0.0-beta.6", + "csstype": "^3.1.1" + }, + "peerDependencies": { + "@onfido/castor-icons": ">=1.0.0" + } + }, + "node_modules/@onfido/castor-icons": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@onfido/castor-icons/-/castor-icons-2.22.0.tgz", + "integrity": "sha512-7OnCvu5xqVWcBLqovZyb99NP0oHw7sjkVYXZhi438i0U6Pgecrhu/14Gc/IN/kvgDxWj9qmiYdd0qdjNaVckrQ==", + "peerDependencies": { + "react": ">=17 || ^16.14 || ^15.7 || ^0.14.10" + } + }, + "node_modules/@onfido/castor-tokens": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@onfido/castor-tokens/-/castor-tokens-1.0.0-beta.6.tgz", + "integrity": "sha512-MfwuSlNdM0Ay0cI3LLyqZGsHW0e1Y1R/0IdQKVU575PdWQx1Q/538aOZMo/a3/oSW0pMEgfOm+mNqPx057cvWA==" + }, + "node_modules/@onfido/opencv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@onfido/opencv/-/opencv-2.1.1.tgz", + "integrity": "sha512-Bwo0YsZrrdm+p5hpNFZ7yrqNVWJxOUbQW9aWDEUtkDWUL+nX2RHIR6F4lBGVmbqnG24anadS/+nEvy80SwD3tQ==", + "dependencies": { + "mirada": "^0.0.15" + } + }, "node_modules/@onfido/react-native-sdk": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz", @@ -10331,6 +10487,78 @@ "join-component": "^1.1.0" } }, + "node_modules/@sentry/browser": { + "version": "7.11.1", + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/core": "7.11.1", + "@sentry/types": "7.11.1", + "@sentry/utils": "7.11.1", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/core": { + "version": "7.11.1", + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "7.11.1", + "@sentry/types": "7.11.1", + "@sentry/utils": "7.11.1", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/hub": { + "version": "7.11.1", + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "7.11.1", + "@sentry/utils": "7.11.1", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/types": { + "version": "7.11.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.11.1", + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "7.11.1", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, "node_modules/@shopify/flash-list": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.6.3.tgz", @@ -10409,6 +10637,11 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@storybook/addon-a11y": { "version": "6.5.10", "dev": true, @@ -19772,6 +20005,88 @@ "node": ">=10" } }, + "node_modules/@tensorflow-models/face-detection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tensorflow-models/face-detection/-/face-detection-1.0.2.tgz", + "integrity": "sha512-anjSxy3MnZdTiVluOEQZeaFWM30IPswFM+SltX6wseXKja/AbrHYqamGNZKUylAs2JAyudq+xqTRPS+nA2ourg==", + "dependencies": { + "rimraf": "^3.0.2", + "tslib": "2.4.0" + }, + "peerDependencies": { + "@mediapipe/face_detection": "~0.4.0", + "@tensorflow/tfjs-backend-webgl": "^4.4.0", + "@tensorflow/tfjs-converter": "^4.4.0", + "@tensorflow/tfjs-core": "^4.4.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.16.0.tgz", + "integrity": "sha512-bQFu7FTUgqgss1AwnqSwQ1f02IPrfLLc2lLn5pyyVrS6Ex7zA6Y4YkfktqoJSRE6LlRZv3vxSriUGE1avRe4qQ==", + "peer": true, + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.16.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.16.0.tgz", + "integrity": "sha512-cIGZWuY892iwTRokbDj3qsLi0AlpQn+U7rzB1mddhHrWr9kBXrrnAvIq0h2aiFzRFNePWUcsbgK+HmYG32kosg==", + "peer": true, + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "4.16.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.16.0" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.16.0.tgz", + "integrity": "sha512-gd8dHl9tqEPQOHZLAUza713nKr42rpvUXrtm7yUhk10THvJT6TXe9Q2AJKmni8J3vfR+ghsCh77F8D4RbShx1Q==", + "peer": true, + "peerDependencies": { + "@tensorflow/tfjs-core": "4.16.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.16.0.tgz", + "integrity": "sha512-MarAtO+Up6wA8pI9QDpQOwwJgb/imYMN++tsoaalyOEE9+B5HS4lQldxDJKXO8Frf4DyXf4FItJktEXaiPfRHw==", + "peer": true, + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.7.0", + "@types/seedrandom": "^2.4.28", + "@webgpu/types": "0.1.38", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "peer": true + }, "node_modules/@testing-library/jest-native": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-native/-/jest-native-5.4.1.tgz", @@ -20261,6 +20576,11 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, + "node_modules/@types/emscripten": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-0.0.34.tgz", + "integrity": "sha512-QSb9ojDincskc+uKMI0KXp8e1NALFINCrMlp8VGKGcTSxeEyRTTKyjWw75NYrCZHUsVEEEpr1tYHpbtaC++/sQ==" + }, "node_modules/@types/eslint": { "version": "8.4.6", "license": "MIT", @@ -20505,6 +20825,11 @@ "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", "dev": true }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/mapbox-gl": { "version": "2.7.13", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.13.tgz", @@ -20577,6 +20902,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -20734,6 +21064,11 @@ "version": "0.16.2", "license": "MIT" }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==" + }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -20833,6 +21168,16 @@ "dev": true, "optional": true }, + "node_modules/@types/webgl-ext": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", + "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" + }, + "node_modules/@types/webgl2": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", + "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" + }, "node_modules/@types/webpack": { "version": "4.41.32", "dev": true, @@ -21850,6 +22195,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", + "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", + "peer": true + }, "node_modules/@webpack-cli/configtest": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", @@ -24278,6 +24629,12 @@ "bluebird": "^3.5.5" } }, + "node_modules/blueimp-load-image": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/blueimp-load-image/-/blueimp-load-image-2.29.0.tgz", + "integrity": "sha512-psm81GlZ0ffKxVT0QN9dvhpzXMv1KxgXSg8ars0XGAcEGsTwFT2IPo59HDXlw4Lo2oImdPzwrwkliZSiLLUpIw==", + "license": "MIT" + }, "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -28380,6 +28737,10 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "2.3.10", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -28898,6 +29259,46 @@ "objectorarray": "^1.0.5" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -28938,6 +29339,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/enumerate-devices": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/enumerate-devices/-/enumerate-devices-1.1.1.tgz", + "integrity": "sha512-8zDbrc7ocusTL1ZGmvgy0cTwdyCaM7sGZoYLRmnWJalLQzmftDtce+uDU91gafOTo9MCtgjSIxyMv/F4+Hcchw==", + "license": "MIT" + }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -30334,6 +30741,12 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-2.2.2.tgz", + "integrity": "sha512-AmQ734LWUB9Iyk+2WIU3Z8iRhdL1XQihEE0iF/QC5Xp11zST0Z5tn5jRHa/PgIld2QIPSCys3CREqOQLUhNvkw==", + "license": "MIT" + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -31410,6 +31823,14 @@ "url": "https://opencollective.com/ramda" } }, + "node_modules/file-type": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", + "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==", + "engines": { + "node": ">=8" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -32985,6 +33406,19 @@ "node": ">= 8" } }, + "node_modules/history": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.5.1.tgz", + "integrity": "sha512-gfHeJeYeMzFtos61gdA1AloO0hGXPF2Yum+2FRdJvlylYQOz51OnT1zuwg9UYst1BRrONhcAh3Nmsg9iblgl6g==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.1", + "loose-envify": "^1.2.0", + "resolve-pathname": "^2.0.0", + "value-equal": "^0.2.0", + "warning": "^3.0.0" + } + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -38053,6 +38487,13 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" }, + "node_modules/js-cookie": { + "version": "3.0.1", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -39060,6 +39501,11 @@ "node": ">=6" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -40674,6 +41120,22 @@ "node": ">= 8" } }, + "node_modules/mirada": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/mirada/-/mirada-0.0.15.tgz", + "integrity": "sha512-mbm4c+wjBVcmUzHRLv/TfOAq+iy03D24KwGxx8H+NSXkD5EOZV9zFWbVxTvZCc9XwR0FIUhryU/kQm12SMSQ3g==", + "dependencies": { + "buffer": "^5.4.3", + "cross-fetch": "^3.0.4", + "file-type": "^12.3.0", + "misc-utils-of-mine-generic": "^0.2.31" + } + }, + "node_modules/misc-utils-of-mine-generic": { + "version": "0.2.45", + "resolved": "https://registry.npmjs.org/misc-utils-of-mine-generic/-/misc-utils-of-mine-generic-0.2.45.tgz", + "integrity": "sha512-WsG2zYiui2cdEbHF2pXmJfnjHb4zL+cy+PaYcLgIpMju98hwX89VbjlvGIfamCfEodbQ0qjCEvD3ocgkCXfMOQ==" + }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -41685,9 +42147,41 @@ } }, "node_modules/onfido-sdk-ui": { - "version": "14.15.0", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-14.15.0.tgz", - "integrity": "sha512-4Z+tnH6pQjK4SyazlzJq17NXO8AnhGcwEACbA3PVbAo90LBpGu1WAZ1r6VidlxFr/oPbu6sg/hisYvfXiqOtTg==" + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-13.6.1.tgz", + "integrity": "sha512-EcFqTN9uaVINRUttSdt6ySUBlfg25dE9f2yxxXVUmrM9a4M1luv+aICej1zE3vRZPFEuFJ9mqJZQUTYo0YMFyg==", + "dependencies": { + "@onfido/active-video-capture": "^0.28.2", + "@onfido/opencv": "^2.0.0", + "@sentry/browser": "^7.2.0", + "blueimp-load-image": "~2.29.0", + "classnames": "~2.2.5", + "core-js": "^3.21.1", + "deepmerge": "^4.2.2", + "dompurify": "^2.2.6", + "enumerate-devices": "^1.1.1", + "eventemitter2": "~2.2.2", + "history": "~4.5.1", + "hoist-non-react-statics": "^3.3.2", + "js-cookie": "^3.0.1", + "pdfobject": "^2.2.7", + "preact": "10.11.3", + "redux": "^4.0.5", + "socket.io-client": "^4.2.0", + "supports-webp": "~1.0.3", + "uuid": "^8.3.2", + "visibilityjs": "~1.2.4", + "xstate": "^4.33.6" + }, + "bin": { + "migrate_locales": "scripts/migrate_locales.js" + } + }, + "node_modules/onfido-sdk-ui/node_modules/classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", + "license": "MIT" }, "node_modules/open": { "version": "8.4.2", @@ -42537,6 +43031,10 @@ "canvas": "^2.11.2" } }, + "node_modules/pdfobject": { + "version": "2.2.8", + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -42923,6 +43421,15 @@ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -44088,6 +44595,16 @@ "react-native-reanimated": ">=2.8.0" } }, + "node_modules/react-native-flipper": { + "version": "0.159.0", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": ">0.62.0" + } + }, "node_modules/react-native-fs": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", @@ -44320,6 +44837,17 @@ "react-native": "*" } }, + "node_modules/react-native-performance-flipper-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-native-performance-flipper-reporter/-/react-native-performance-flipper-reporter-2.0.0.tgz", + "integrity": "sha512-ccOgq99eK3OvrNNhpJDC4ydNk/1JGgWZPo2FLrPDLUHXAR4EcE9cUAtb46oGOpvHk5ZOb5aEDofc/CS9OEGcag==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react-native-flipper": "*", + "react-native-performance": "*" + } + }, "node_modules/react-native-permissions": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.9.3.tgz", @@ -44409,33 +44937,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/react-native-release-profiler": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.1.6.tgz", - "integrity": "sha512-kSAPYjO3PDzV4xbjgj2NoiHtL7EaXmBira/WOcyz6S7mz1MVBoF0Bj74z5jAZo6BoBJRKqmQWI4ep+m0xvoF+g==", - "dependencies": { - "@react-native-community/cli": "^12.2.1", - "commander": "^11.1.0" - }, - "bin": { - "react-native-release-profiler": "lib/commonjs/cli.js" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, - "node_modules/react-native-release-profiler/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "engines": { - "node": ">=16" - } - }, "node_modules/react-native-render-html": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.1.tgz", @@ -44505,14 +45006,6 @@ "react-native": "*" } }, - "node_modules/react-native-share": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.0.2.tgz", - "integrity": "sha512-EZs4MtsyauAI1zP8xXT1hIFB/pXOZJNDCKcgCpEfTZFXgCUzz8MDVbI1ocP2hA59XHRSkqAQdbJ0BFTpjxOBlg==", - "engines": { - "node": ">=16" - } - }, "node_modules/react-native-sound": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.11.2.tgz", @@ -46421,6 +46914,12 @@ "node": ">=8" } }, + "node_modules/resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==", + "license": "MIT" + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -46789,7 +47288,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "dev": true, "license": "MIT" }, "node_modules/select": { @@ -47688,6 +48186,32 @@ "node": ">=0.10.0" } }, + "node_modules/socket.io-client": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", + "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -48748,6 +49272,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/supports-webp": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/supports-webp/-/supports-webp-1.0.7.tgz", + "integrity": "sha512-ZlqT+sCgZKcykOLrk8DYR4t3Em+nyVSHpiV3q7uzOutLwKIYU23n88KibCLw3FzM4NCQeRorvZ55AV/77lQyOQ==", + "license": "MIT" + }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -50884,6 +51414,12 @@ "builtins": "^1.0.3" } }, + "node_modules/value-equal": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.2.1.tgz", + "integrity": "sha512-yRL36Xb2K/HmFT5Fe3M86S7mu4+a12/3l7uytUh6eNPPjP77ldPBvsAvmnWff39sXn55naRMZN8LZWRO8PWaeQ==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -50951,6 +51487,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/visibilityjs": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/visibilityjs/-/visibilityjs-1.2.8.tgz", + "integrity": "sha512-Y+aL3OUX88b+/VSmkmC2ApuLbf0grzbNLpCfIDSw3BzTU6PqcPsdgIOaw8b+eZoy+DdQqnVN3y/Evow9vQq9Ig==", + "license": "MIT" + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -51012,6 +51554,15 @@ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", "license": "MIT" }, + "node_modules/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -52445,6 +52996,22 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xstate": { + "version": "4.37.2", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", From 790a1915cbc7c4558688b753efda89ad84c32b44 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 12 Mar 2024 10:14:23 +0100 Subject: [PATCH 17/40] fix package-lock --- package-lock.json | 508 ++++------------------------------------------ 1 file changed, 44 insertions(+), 464 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81686286bf1c..bc373abcd9b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.50-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.49-4", + "version": "1.4.50-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -64,7 +64,7 @@ "lodash": "4.17.21", "lottie-react-native": "6.4.1", "mapbox-gl": "^2.15.0", - "onfido-sdk-ui": "13.6.1", + "onfido-sdk-ui": "14.15.0", "patch-package": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", @@ -107,9 +107,11 @@ "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", "react-native-reanimated": "^3.7.2", + "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", "react-native-tab-view": "^3.5.2", @@ -227,7 +229,6 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", - "react-native-performance-flipper-reporter": "^2.0.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", "setimmediate": "^1.0.5", @@ -7713,16 +7714,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@mediapipe/face_detection": { - "version": "0.4.1646425229", - "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", - "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==" - }, - "node_modules/@mediapipe/face_mesh": { - "version": "0.4.1633559619", - "resolved": "https://registry.npmjs.org/@mediapipe/face_mesh/-/face_mesh-0.4.1633559619.tgz", - "integrity": "sha512-Vc8cdjxS5+O2gnjWH9KncYpUCVXT0h714KlWAsyqJvJbIgUJBqpppbIx8yWcAzBDxm/5cYSuBI5p5ySIPxzcEg==" - }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -10487,78 +10478,6 @@ "join-component": "^1.1.0" } }, - "node_modules/@sentry/browser": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/core": "7.11.1", - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/core": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/hub": "7.11.1", - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/hub": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/types": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, "node_modules/@shopify/flash-list": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.6.3.tgz", @@ -10637,11 +10556,6 @@ "@sinonjs/commons": "^2.0.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, "node_modules/@storybook/addon-a11y": { "version": "6.5.10", "dev": true, @@ -20005,88 +19919,6 @@ "node": ">=10" } }, - "node_modules/@tensorflow-models/face-detection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-detection/-/face-detection-1.0.2.tgz", - "integrity": "sha512-anjSxy3MnZdTiVluOEQZeaFWM30IPswFM+SltX6wseXKja/AbrHYqamGNZKUylAs2JAyudq+xqTRPS+nA2ourg==", - "dependencies": { - "rimraf": "^3.0.2", - "tslib": "2.4.0" - }, - "peerDependencies": { - "@mediapipe/face_detection": "~0.4.0", - "@tensorflow/tfjs-backend-webgl": "^4.4.0", - "@tensorflow/tfjs-converter": "^4.4.0", - "@tensorflow/tfjs-core": "^4.4.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.16.0.tgz", - "integrity": "sha512-bQFu7FTUgqgss1AwnqSwQ1f02IPrfLLc2lLn5pyyVrS6Ex7zA6Y4YkfktqoJSRE6LlRZv3vxSriUGE1avRe4qQ==", - "peer": true, - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.16.0.tgz", - "integrity": "sha512-cIGZWuY892iwTRokbDj3qsLi0AlpQn+U7rzB1mddhHrWr9kBXrrnAvIq0h2aiFzRFNePWUcsbgK+HmYG32kosg==", - "peer": true, - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.16.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-converter": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.16.0.tgz", - "integrity": "sha512-gd8dHl9tqEPQOHZLAUza713nKr42rpvUXrtm7yUhk10THvJT6TXe9Q2AJKmni8J3vfR+ghsCh77F8D4RbShx1Q==", - "peer": true, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-core": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.16.0.tgz", - "integrity": "sha512-MarAtO+Up6wA8pI9QDpQOwwJgb/imYMN++tsoaalyOEE9+B5HS4lQldxDJKXO8Frf4DyXf4FItJktEXaiPfRHw==", - "peer": true, - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.7.0", - "@types/seedrandom": "^2.4.28", - "@webgpu/types": "0.1.38", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "peer": true - }, "node_modules/@testing-library/jest-native": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-native/-/jest-native-5.4.1.tgz", @@ -20576,11 +20408,6 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, - "node_modules/@types/emscripten": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-0.0.34.tgz", - "integrity": "sha512-QSb9ojDincskc+uKMI0KXp8e1NALFINCrMlp8VGKGcTSxeEyRTTKyjWw75NYrCZHUsVEEEpr1tYHpbtaC++/sQ==" - }, "node_modules/@types/eslint": { "version": "8.4.6", "license": "MIT", @@ -20825,11 +20652,6 @@ "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", "dev": true }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "node_modules/@types/mapbox-gl": { "version": "2.7.13", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.13.tgz", @@ -20902,11 +20724,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" - }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -21064,11 +20881,6 @@ "version": "0.16.2", "license": "MIT" }, - "node_modules/@types/seedrandom": { - "version": "2.4.34", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", - "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==" - }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -21168,16 +20980,6 @@ "dev": true, "optional": true }, - "node_modules/@types/webgl-ext": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", - "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" - }, - "node_modules/@types/webgl2": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" - }, "node_modules/@types/webpack": { "version": "4.41.32", "dev": true, @@ -22195,12 +21997,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webgpu/types": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", - "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", - "peer": true - }, "node_modules/@webpack-cli/configtest": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", @@ -24629,12 +24425,6 @@ "bluebird": "^3.5.5" } }, - "node_modules/blueimp-load-image": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/blueimp-load-image/-/blueimp-load-image-2.29.0.tgz", - "integrity": "sha512-psm81GlZ0ffKxVT0QN9dvhpzXMv1KxgXSg8ars0XGAcEGsTwFT2IPo59HDXlw4Lo2oImdPzwrwkliZSiLLUpIw==", - "license": "MIT" - }, "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -28737,10 +28527,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "2.3.10", - "license": "(MPL-2.0 OR Apache-2.0)" - }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -29259,46 +29045,6 @@ "objectorarray": "^1.0.5" } }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -29339,12 +29085,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/enumerate-devices": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/enumerate-devices/-/enumerate-devices-1.1.1.tgz", - "integrity": "sha512-8zDbrc7ocusTL1ZGmvgy0cTwdyCaM7sGZoYLRmnWJalLQzmftDtce+uDU91gafOTo9MCtgjSIxyMv/F4+Hcchw==", - "license": "MIT" - }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -30741,12 +30481,6 @@ "node": ">=6" } }, - "node_modules/eventemitter2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-2.2.2.tgz", - "integrity": "sha512-AmQ734LWUB9Iyk+2WIU3Z8iRhdL1XQihEE0iF/QC5Xp11zST0Z5tn5jRHa/PgIld2QIPSCys3CREqOQLUhNvkw==", - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -31823,14 +31557,6 @@ "url": "https://opencollective.com/ramda" } }, - "node_modules/file-type": { - "version": "12.4.2", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", - "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==", - "engines": { - "node": ">=8" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -33406,19 +33132,6 @@ "node": ">= 8" } }, - "node_modules/history": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.5.1.tgz", - "integrity": "sha512-gfHeJeYeMzFtos61gdA1AloO0hGXPF2Yum+2FRdJvlylYQOz51OnT1zuwg9UYst1BRrONhcAh3Nmsg9iblgl6g==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.0.0", - "value-equal": "^0.2.0", - "warning": "^3.0.0" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -38487,13 +38200,6 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" }, - "node_modules/js-cookie": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -39501,11 +39207,6 @@ "node": ">=6" } }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -41120,22 +40821,6 @@ "node": ">= 8" } }, - "node_modules/mirada": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/mirada/-/mirada-0.0.15.tgz", - "integrity": "sha512-mbm4c+wjBVcmUzHRLv/TfOAq+iy03D24KwGxx8H+NSXkD5EOZV9zFWbVxTvZCc9XwR0FIUhryU/kQm12SMSQ3g==", - "dependencies": { - "buffer": "^5.4.3", - "cross-fetch": "^3.0.4", - "file-type": "^12.3.0", - "misc-utils-of-mine-generic": "^0.2.31" - } - }, - "node_modules/misc-utils-of-mine-generic": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/misc-utils-of-mine-generic/-/misc-utils-of-mine-generic-0.2.45.tgz", - "integrity": "sha512-WsG2zYiui2cdEbHF2pXmJfnjHb4zL+cy+PaYcLgIpMju98hwX89VbjlvGIfamCfEodbQ0qjCEvD3ocgkCXfMOQ==" - }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -42147,41 +41832,9 @@ } }, "node_modules/onfido-sdk-ui": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-13.6.1.tgz", - "integrity": "sha512-EcFqTN9uaVINRUttSdt6ySUBlfg25dE9f2yxxXVUmrM9a4M1luv+aICej1zE3vRZPFEuFJ9mqJZQUTYo0YMFyg==", - "dependencies": { - "@onfido/active-video-capture": "^0.28.2", - "@onfido/opencv": "^2.0.0", - "@sentry/browser": "^7.2.0", - "blueimp-load-image": "~2.29.0", - "classnames": "~2.2.5", - "core-js": "^3.21.1", - "deepmerge": "^4.2.2", - "dompurify": "^2.2.6", - "enumerate-devices": "^1.1.1", - "eventemitter2": "~2.2.2", - "history": "~4.5.1", - "hoist-non-react-statics": "^3.3.2", - "js-cookie": "^3.0.1", - "pdfobject": "^2.2.7", - "preact": "10.11.3", - "redux": "^4.0.5", - "socket.io-client": "^4.2.0", - "supports-webp": "~1.0.3", - "uuid": "^8.3.2", - "visibilityjs": "~1.2.4", - "xstate": "^4.33.6" - }, - "bin": { - "migrate_locales": "scripts/migrate_locales.js" - } - }, - "node_modules/onfido-sdk-ui/node_modules/classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "license": "MIT" + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-14.15.0.tgz", + "integrity": "sha512-4Z+tnH6pQjK4SyazlzJq17NXO8AnhGcwEACbA3PVbAo90LBpGu1WAZ1r6VidlxFr/oPbu6sg/hisYvfXiqOtTg==" }, "node_modules/open": { "version": "8.4.2", @@ -43031,10 +42684,6 @@ "canvas": "^2.11.2" } }, - "node_modules/pdfobject": { - "version": "2.2.8", - "license": "MIT" - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -43421,15 +43070,6 @@ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, - "node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -44595,16 +44235,6 @@ "react-native-reanimated": ">=2.8.0" } }, - "node_modules/react-native-flipper": { - "version": "0.159.0", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": ">0.62.0" - } - }, "node_modules/react-native-fs": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", @@ -44837,17 +44467,6 @@ "react-native": "*" } }, - "node_modules/react-native-performance-flipper-reporter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-native-performance-flipper-reporter/-/react-native-performance-flipper-reporter-2.0.0.tgz", - "integrity": "sha512-ccOgq99eK3OvrNNhpJDC4ydNk/1JGgWZPo2FLrPDLUHXAR4EcE9cUAtb46oGOpvHk5ZOb5aEDofc/CS9OEGcag==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react-native-flipper": "*", - "react-native-performance": "*" - } - }, "node_modules/react-native-permissions": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.9.3.tgz", @@ -44937,6 +44556,33 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/react-native-release-profiler": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.1.6.tgz", + "integrity": "sha512-kSAPYjO3PDzV4xbjgj2NoiHtL7EaXmBira/WOcyz6S7mz1MVBoF0Bj74z5jAZo6BoBJRKqmQWI4ep+m0xvoF+g==", + "dependencies": { + "@react-native-community/cli": "^12.2.1", + "commander": "^11.1.0" + }, + "bin": { + "react-native-release-profiler": "lib/commonjs/cli.js" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-release-profiler/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-render-html": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.1.tgz", @@ -45006,6 +44652,14 @@ "react-native": "*" } }, + "node_modules/react-native-share": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.0.2.tgz", + "integrity": "sha512-EZs4MtsyauAI1zP8xXT1hIFB/pXOZJNDCKcgCpEfTZFXgCUzz8MDVbI1ocP2hA59XHRSkqAQdbJ0BFTpjxOBlg==", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-sound": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.11.2.tgz", @@ -46914,12 +46568,6 @@ "node": ">=8" } }, - "node_modules/resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==", - "license": "MIT" - }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -47288,6 +46936,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true, "license": "MIT" }, "node_modules/select": { @@ -48186,32 +47835,6 @@ "node": ">=0.10.0" } }, - "node_modules/socket.io-client": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", - "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -49272,12 +48895,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/supports-webp": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/supports-webp/-/supports-webp-1.0.7.tgz", - "integrity": "sha512-ZlqT+sCgZKcykOLrk8DYR4t3Em+nyVSHpiV3q7uzOutLwKIYU23n88KibCLw3FzM4NCQeRorvZ55AV/77lQyOQ==", - "license": "MIT" - }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -51414,12 +51031,6 @@ "builtins": "^1.0.3" } }, - "node_modules/value-equal": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.2.1.tgz", - "integrity": "sha512-yRL36Xb2K/HmFT5Fe3M86S7mu4+a12/3l7uytUh6eNPPjP77ldPBvsAvmnWff39sXn55naRMZN8LZWRO8PWaeQ==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -51487,12 +51098,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/visibilityjs": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/visibilityjs/-/visibilityjs-1.2.8.tgz", - "integrity": "sha512-Y+aL3OUX88b+/VSmkmC2ApuLbf0grzbNLpCfIDSw3BzTU6PqcPsdgIOaw8b+eZoy+DdQqnVN3y/Evow9vQq9Ig==", - "license": "MIT" - }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -51554,15 +51159,6 @@ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", "license": "MIT" }, - "node_modules/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", - "license": "BSD-3-Clause", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -52996,22 +52592,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xstate": { - "version": "4.37.2", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/xstate" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", From 40a9715111b164b8ca073aa7aef7e69ac58fe318 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Mar 2024 11:46:17 +0100 Subject: [PATCH 18/40] wrap parsing phone number in a function --- src/libs/OptionsListUtils.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 8d96757e6e95..b66687f392ff 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1373,6 +1373,16 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u ); } +/** + * Checks if string is a valid phone number and it returns the formatted phone number if so + */ +function parsePhoneNumber(text: string) { + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(text))); + const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : text.toLowerCase(); + + return output; +} + /** * Build the options */ @@ -1471,8 +1481,7 @@ function getOptions( let recentReportOptions = []; let personalDetailsOptions: ReportUtils.OptionData[] = []; const reportMapForAccountIDs: Record = {}; - const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const searchValue = parsePhoneNumber(searchInputValue); // Filter out all the reports that shouldn't be displayed const filteredReports = Object.values(reports ?? {}).filter((report) => { @@ -2057,9 +2066,11 @@ function formatSectionsFromSearchTerm( }; } +/** + * Filters options based on the search input value + */ function filterOptions(options: Partial, searchInputValue: string): ReportUtils.OptionData[] { - const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const searchValue = parsePhoneNumber(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; const reportsByType = (options.recentReports ?? []).reduce( From 3223b350080012d6b275da20f24c448b13f67cdc Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Mar 2024 12:04:11 +0100 Subject: [PATCH 19/40] fix tests --- src/libs/OptionsListUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b66687f392ff..355f4e3eb867 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1380,7 +1380,7 @@ function parsePhoneNumber(text: string) { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(text))); const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : text.toLowerCase(); - return output; + return {output, parsedPhoneNumber}; } /** @@ -1481,7 +1481,7 @@ function getOptions( let recentReportOptions = []; let personalDetailsOptions: ReportUtils.OptionData[] = []; const reportMapForAccountIDs: Record = {}; - const searchValue = parsePhoneNumber(searchInputValue); + const {output: searchValue, parsedPhoneNumber} = parsePhoneNumber(searchInputValue); // Filter out all the reports that shouldn't be displayed const filteredReports = Object.values(reports ?? {}).filter((report) => { @@ -2070,7 +2070,7 @@ function formatSectionsFromSearchTerm( * Filters options based on the search input value */ function filterOptions(options: Partial, searchInputValue: string): ReportUtils.OptionData[] { - const searchValue = parsePhoneNumber(searchInputValue); + const {output: searchValue} = parsePhoneNumber(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; const reportsByType = (options.recentReports ?? []).reduce( From 15ac693e7e4dc8faa16876b657dc1b7e31e63b12 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Mar 2024 21:20:32 +0100 Subject: [PATCH 20/40] rewrite filtering function --- src/libs/OptionsListUtils.ts | 112 +++++++++++++++++++-------------- src/pages/SearchPage/index.tsx | 14 +++-- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 355f4e3eb867..9fee528abaac 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -171,8 +171,6 @@ type GetOptions = { type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; -type ReportTypesOptionData = Record; - /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can * be configured to display different results based on the options passed to the private getOptions() method. Public @@ -641,6 +639,7 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, + visibleChatMemberAccountIDs: [], }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -2069,26 +2068,10 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions(options: Partial, searchInputValue: string): ReportUtils.OptionData[] { +function filterOptions(options: GetOptions, searchInputValue: string): GetOptions { const {output: searchValue} = parsePhoneNumber(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; - const reportsByType = (options.recentReports ?? []).reduce( - (acc, option) => { - if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { - acc.chatRoomsAndPolicyExpenseChats.push(option); - } else { - acc.reports.push(option); - } - - return acc; - }, - { - chatRoomsAndPolicyExpenseChats: [], - reports: [], - personalDetails: options.personalDetails ?? [], - }, - ); const createFilter = (items: ReportUtils.OptionData[], keys: ReadonlyArray>, term: string) => filterArrayByMatch(items, term, { keys, @@ -2099,42 +2082,79 @@ function filterOptions(options: Partial, searchInputValue: string): // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) const emailRegex = /\.(?=[^\s@]*@)/g; + const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { + const keys: string[] = []; + const visibleChatMemberAccountIDs = item.participantsList ?? []; + if (allPersonalDetails) { + visibleChatMemberAccountIDs.forEach((participant) => { + const login = participant?.login; + + if (login) { + keys.push(login); + keys.push(login.replace(emailRegex, '')); + } + }); + } + + return keys; + }; + const matchResults = searchTerms.reduceRight((items, term) => { - const personalDetails = createFilter( - items.personalDetails, + const recentReports = createFilter( + items.recentReports ?? [], [ - 'text', - 'login', - (item) => (item.login ? item.login.replace(emailRegex, '') : []), - 'participantsList.0.displayName', - 'participantsList.0.firstName', - 'participantsList.0.lastName', - ], - term, - ); - const chatRoomsAndPolicyExpenseChats = createFilter(items.chatRoomsAndPolicyExpenseChats, ['text', 'alternateText'], term); - const reports = createFilter( - items.reports, - [ - 'text', - 'participantsList.0.login', - (item) => (item.participantsList ? item.participantsList.filter(({login}) => login).map((i) => (i.login ? i.login.replace(emailRegex, '') : '')) : []), - 'participantsList.0.displayName', - 'participantsList.0.firstName', - 'participantsList.0.lastName', + (item) => { + let keys: string[] = []; + + keys.push(item.text ?? ''); + + if (item.login) { + keys.push(item.login); + keys.push(item.login.replace(emailRegex, '')); + } + + if (item.isThread) { + if (item.alternateText) { + keys.push(item.alternateText); + + keys = keys.concat(getParticipantsLoginsArray(item)); + } + } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { + if (item.subtitle) { + keys.push(item.subtitle); + } + } else { + keys = keys.concat(getParticipantsLoginsArray(item)); + } + return keys; + }, ], term, ); + const personalDetails = createFilter(items.personalDetails ?? [], ['participantsList.0.displayName', 'login', (item) => item.login?.replace(emailRegex, '') ?? ''], term); return { - reports, - personalDetails, - chatRoomsAndPolicyExpenseChats, + recentReports: recentReports ?? ([] as ReportUtils.OptionData[]), + personalDetails: personalDetails ?? ([] as ReportUtils.OptionData[]), + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], }; - }, reportsByType); + }, options); - const filteredOptions = Object.values(matchResults).flat(); - return orderOptions(filteredOptions, searchValue); + const recentReports = matchResults.recentReports.concat(matchResults.personalDetails); + + return { + personalDetails: [], + recentReports: orderOptions(recentReports, searchValue), + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; } export { diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index f0b982b3d4ce..b106336df082 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -40,6 +40,8 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; +type Options = OptionsListUtils.GetOptions & {headerMessage: string}; + type SearchPageSectionItem = { data: ReportUtils.OptionData[]; shouldShow: boolean; @@ -75,12 +77,16 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP Report.searchInServer(debouncedSearchValue.trim()); }, [debouncedSearchValue]); - const searchOptions = useMemo(() => { + const searchOptions: Options = useMemo(() => { if (!isScreenTransitionEnd) { return { recentReports: [], personalDetails: [], userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], headerMessage: '', }; } @@ -100,10 +106,10 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP } const options = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue); - const header = OptionsListUtils.getHeaderMessage(options.length > 0, false, debouncedSearchValue); + const header = OptionsListUtils.getHeaderMessage(options.recentReports.length > 0, false, debouncedSearchValue); return { - recentReports: options, - personalDetails: [], + recentReports: options.recentReports, + personalDetails: options.personalDetails, userToInvite: null, headerMessage: header, }; From f370b0299a8b487443c3feb553363d7fff9f97fa Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Mar 2024 21:30:39 +0100 Subject: [PATCH 21/40] update phone parsing function --- src/libs/OptionsListUtils.ts | 27 +++++++++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- .../WorkspaceWorkflowsApproverPage.tsx | 2 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fac1f12d6731..ffc95e58d40c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -809,7 +809,12 @@ function getEnabledCategoriesCount(options: PolicyCategories): number { function getSearchValueForPhoneOrEmail(searchTerm: string) { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm))); - return parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); + const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); + + return { + output, + parsedPhoneNumber, + }; } /** @@ -1372,15 +1377,15 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u ); } -/** - * Checks if string is a valid phone number and it returns the formatted phone number if so - */ -function parsePhoneNumber(text: string) { - const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(text))); - const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : text.toLowerCase(); +// /** +// * Checks if string is a valid phone number and it returns the formatted phone number if so +// */ +// function parsePhoneNumber(text: string) { +// const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(text))); +// const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : text.toLowerCase(); - return {output, parsedPhoneNumber}; -} +// return {output, parsedPhoneNumber}; +// } /** * Build the options @@ -1480,7 +1485,7 @@ function getOptions( let recentReportOptions = []; let personalDetailsOptions: ReportUtils.OptionData[] = []; const reportMapForAccountIDs: Record = {}; - const {output: searchValue, parsedPhoneNumber} = parsePhoneNumber(searchInputValue); + const {output: searchValue, parsedPhoneNumber} = getSearchValueForPhoneOrEmail(searchInputValue); // Filter out all the reports that shouldn't be displayed const filteredReports = Object.values(reports ?? {}).filter((report) => { @@ -2069,7 +2074,7 @@ function formatSectionsFromSearchTerm( * Filters options based on the search input value */ function filterOptions(options: GetOptions, searchInputValue: string): GetOptions { - const {output: searchValue} = parsePhoneNumber(searchInputValue); + const {output: searchValue} = getSearchValueForPhoneOrEmail(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; const createFilter = (items: ReportUtils.OptionData[], keys: ReadonlyArray>, term: string) => diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 00d7eaa33f6b..d4f74eabaada 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -176,7 +176,7 @@ function WorkspaceInvitePage({ const accountID = option.accountID; const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const {output: searchValue} = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index 52406a8033d2..fe05dbc54f7c 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -115,7 +115,7 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, if (searchTerm !== '') { const filteredOptions = [...formattedApprover, ...formattedPolicyMembers].filter((option) => { - const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const {output: searchValue} = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); }); return [ diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index 6da120f95766..ff49aeddb377 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -134,7 +134,7 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta if (searchTerm !== '') { const filteredOptions = [...formattedPolicyAdmins, ...formattedAuthorizedPayer].filter((option) => { - const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const {output: searchValue} = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); }); return [ From 18e28c283396763058b157aedf580d2b44bdea56 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Mar 2024 21:36:41 +0100 Subject: [PATCH 22/40] option list utils cleanup --- src/libs/OptionsListUtils.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ffc95e58d40c..e4111704a5c4 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -639,7 +639,6 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, - visibleChatMemberAccountIDs: [], }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -1377,16 +1376,6 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u ); } -// /** -// * Checks if string is a valid phone number and it returns the formatted phone number if so -// */ -// function parsePhoneNumber(text: string) { -// const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(text))); -// const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : text.toLowerCase(); - -// return {output, parsedPhoneNumber}; -// } - /** * Build the options */ From 8d070e493de373fbc9a888c29799dcb468bf80e9 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 19 Mar 2024 13:25:49 +0100 Subject: [PATCH 23/40] add tests for filtering --- src/libs/OptionsListUtils.ts | 10 +++- tests/unit/OptionsListUtilsTest.js | 95 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e4111704a5c4..1edadcb27bf4 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2083,6 +2083,10 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption visibleChatMemberAccountIDs.forEach((participant) => { const login = participant?.login; + if (participant?.displayName) { + keys.push(participant.displayName); + } + if (login) { keys.push(login); keys.push(login.replace(emailRegex, '')); @@ -2110,9 +2114,8 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption if (item.isThread) { if (item.alternateText) { keys.push(item.alternateText); - - keys = keys.concat(getParticipantsLoginsArray(item)); } + keys = keys.concat(getParticipantsLoginsArray(item)); } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { if (item.subtitle) { keys.push(item.subtitle); @@ -2120,7 +2123,8 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption } else { keys = keys.concat(getParticipantsLoginsArray(item)); } - return keys; + + return uniqFast(keys); }, ], term, diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 7244b7830a29..cb73d55e5fb2 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -249,6 +249,21 @@ describe('OptionsListUtils', () => { }, }; + const REPORTS_WITH_CHAT_ROOM = { + ...REPORTS, + 15: { + lastReadTime: '2021-01-14 11:25:39.301', + lastVisibleActionCreated: '2022-11-22 03:26:02.000', + isPinned: false, + reportID: 15, + participantAccountIDs: [3, 4], + visibleChatMemberAccountIDs: [3, 4], + reportName: 'Spider-Man, Black Panther', + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, + }, + }; + const PERSONAL_DETAILS_WITH_CONCIERGE = { ...PERSONAL_DETAILS, @@ -2198,4 +2213,84 @@ describe('OptionsListUtils', () => { // `isDisabled` is always false expect(_.every(formattedMembers, (personalDetail) => !personalDetail.isDisabled)).toBe(true); }); + + describe('filterOptions', () => { + it('should return all options when search is empty', () => { + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const filteredOptions = OptionsListUtils.filterOptions(options, ''); + + expect(options.recentReports.length + options.personalDetails.length).toBe(filteredOptions.recentReports.length); + }); + + it('should return filtered options in correct order', () => { + const searchText = 'man'; + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + expect(filteredOptions.recentReports.length).toBe(4); + expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman'); + expect(filteredOptions.recentReports[1].text).toBe('Spider-Man'); + expect(filteredOptions.recentReports[2].text).toBe('Black Widow'); + expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic'); + }); + + it('should filter users by email', () => { + const searchText = 'mistersinister@marauders.com'; + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.recentReports.length).toBe(1); + expect(filteredOptions.recentReports[0].text).toBe('Mr Sinister'); + }); + + it('should find archived chats', () => { + const searchText = 'Archived'; + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.recentReports.length).toBe(1); + expect(filteredOptions.recentReports[0].isArchivedRoom).toBe(true); + }); + + it('should filter options by email if dot is skipped in the email', () => { + const searchText = 'barryallen'; + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, '', [CONST.BETAS.ALL]); + + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.recentReports.length).toBe(1); + expect(filteredOptions.recentReports[0].login).toBe('barry.allen@expensify.com'); + }); + + it('should include workspaces in the search results', () => { + const searchText = 'avengers'; + const options = OptionsListUtils.getSearchOptions(REPORTS_WITH_WORKSPACE_ROOMS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.recentReports.length).toBe(1); + expect(filteredOptions.recentReports[0].subtitle).toBe('Avengers Room'); + }); + + it('should put exact match by login on the top of the list', () => { + const searchText = 'reedrichards@expensify.com'; + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.recentReports.length).toBe(2); + expect(filteredOptions.recentReports[0].login).toBe(searchText); + }); + + it('should prioritize options with matching display name over chatrooms', () => { + const searchText = 'spider'; + const options = OptionsListUtils.getSearchOptions(REPORTS_WITH_CHAT_ROOM, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + + const filterOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filterOptions.recentReports.length).toBe(2); + expect(filterOptions.recentReports[1].isChatRoom).toBe(true); + }); + }); }); From 663627079efbdb21cb427b2d23707aaa6c68f2f3 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 19 Mar 2024 13:31:46 +0100 Subject: [PATCH 24/40] add test checking lastVisibleActionCreated --- tests/unit/OptionsListUtilsTest.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index cb73d55e5fb2..c2ce31d199ae 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -2292,5 +2292,16 @@ describe('OptionsListUtils', () => { expect(filterOptions.recentReports.length).toBe(2); expect(filterOptions.recentReports[1].isChatRoom).toBe(true); }); + + it('should put the item with latest lastVisibleActionCreated on top when search value match multiple items', () => { + const searchText = 'fantastic'; + + const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, ''); + const filteredOptions = OptionsListUtils.filterOptions(options, searchText); + + expect(filteredOptions.recentReports.length).toBe(2); + expect(filteredOptions.recentReports[0].text).toBe('Mister Fantastic'); + expect(filteredOptions.recentReports[1].text).toBe('Mister Fantastic'); + }); }); }); From 99a0cd76d521dd78ef273250cd43e41bb56228f9 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 21 Mar 2024 09:16:49 +0100 Subject: [PATCH 25/40] Revert "update phone parsing function" This reverts commit f370b0299a8b487443c3feb553363d7fff9f97fa. --- src/libs/OptionsListUtils.ts | 12 ++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- .../workflows/WorkspaceWorkflowsApproverPage.tsx | 2 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index c718db6f1207..2ea43aaec26b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -841,12 +841,7 @@ function getEnabledCategoriesCount(options: PolicyCategories): number { function getSearchValueForPhoneOrEmail(searchTerm: string) { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm))); - const output = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); - - return { - output, - parsedPhoneNumber, - }; + return parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); } /** @@ -1505,7 +1500,8 @@ function getOptions( let recentReportOptions = []; let personalDetailsOptions: ReportUtils.OptionData[] = []; const reportMapForAccountIDs: Record = {}; - const {output: searchValue, parsedPhoneNumber} = getSearchValueForPhoneOrEmail(searchInputValue); + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed const filteredReports = Object.values(reports ?? {}).filter((report) => { @@ -2094,7 +2090,7 @@ function formatSectionsFromSearchTerm( * Filters options based on the search input value */ function filterOptions(options: GetOptions, searchInputValue: string): GetOptions { - const {output: searchValue} = getSearchValueForPhoneOrEmail(searchInputValue); + const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; const createFilter = (items: ReportUtils.OptionData[], keys: ReadonlyArray>, term: string) => diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index d4f74eabaada..00d7eaa33f6b 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -176,7 +176,7 @@ function WorkspaceInvitePage({ const accountID = option.accountID; const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - const {output: searchValue} = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index 5fbcbdd1b457..e8913aafe972 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -121,7 +121,7 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, if (searchTerm !== '') { const filteredOptions = [...formattedApprover, ...formattedPolicyMembers].filter((option) => { - const {output: searchValue} = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); }); return [ diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index ff49aeddb377..6da120f95766 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -134,7 +134,7 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta if (searchTerm !== '') { const filteredOptions = [...formattedPolicyAdmins, ...formattedAuthorizedPayer].filter((option) => { - const {output: searchValue} = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); }); return [ From ae77968eebfcdff84b29f003521ebef236a94cd8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 11:08:43 +0200 Subject: [PATCH 26/40] code review updates --- src/libs/OptionsListUtils.ts | 1 - src/libs/filterArrayByMatch.ts | 42 ++++++++++++++++------------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 429649c68ee5..a1dc873440fd 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2042,7 +2042,6 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption const createFilter = (items: ReportUtils.OptionData[], keys: ReadonlyArray>, term: string) => filterArrayByMatch(items, term, { keys, - strict: true, }); // The regex below is used to remove dots only from the local part of the user email (local-part@domain) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index c8b7eb617dfc..afba4e368aed 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -2,8 +2,9 @@ * This file is a slim version of match-sorter library (https://github.com/kentcdodds/match-sorter) adjusted to the needs. Use `threshold` option with one of the rankings defined below to control the strictness of the match. */ +import type {ValueOf} from 'type-fest'; -const rankings = { +const MATCH_RANK = { CASE_SENSITIVE_EQUAL: 7, EQUAL: 6, STARTS_WITH: 5, @@ -14,13 +15,13 @@ const rankings = { NO_MATCH: 0, } as const; -type Ranking = (typeof rankings)[keyof typeof rankings]; +type Ranking = ValueOf; type RankingInfo = { rankedValue: string; rank: Ranking; keyIndex: number; - keyThreshold: Ranking | undefined; + keyThreshold?: Ranking; }; type ValueGetterKey = (item: T) => string | string[]; @@ -41,7 +42,6 @@ type KeyOption = KeyAttributesOptions | ValueGetterKey | string; type Options = { keys?: ReadonlyArray>; threshold?: Ranking; - strict?: boolean; }; type IndexableByString = Record; @@ -169,7 +169,7 @@ function getClosenessRanking(testString: string, stringToRank: string): Ranking function getRanking(spread: number) { const spreadPercentage = 1 / spread; const inOrderPercentage = matchingInOrderCharCount / stringToRank.length; - const ranking = rankings.MATCHES + inOrderPercentage * spreadPercentage; + const ranking = MATCH_RANK.MATCHES + inOrderPercentage * spreadPercentage; return ranking as Ranking; } @@ -177,7 +177,7 @@ function getClosenessRanking(testString: string, stringToRank: string): Ranking const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0); if (firstIndex < 0) { - return rankings.NO_MATCH; + return MATCH_RANK.NO_MATCH; } charNumber = firstIndex; @@ -187,7 +187,7 @@ function getClosenessRanking(testString: string, stringToRank: string): Ranking const found = charNumber > -1; if (!found) { - return rankings.NO_MATCH; + return MATCH_RANK.NO_MATCH; } } @@ -204,12 +204,12 @@ function getClosenessRanking(testString: string, stringToRank: string): Ranking function getMatchRanking(testString: string, stringToRank: string): Ranking { // too long if (stringToRank.length > testString.length) { - return rankings.NO_MATCH; + return MATCH_RANK.NO_MATCH; } // case sensitive equals if (testString === stringToRank) { - return rankings.CASE_SENSITIVE_EQUAL; + return MATCH_RANK.CASE_SENSITIVE_EQUAL; } // Lower casing before further comparison @@ -218,30 +218,30 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { // case insensitive equals if (lowercaseTestString === lowercaseStringToRank) { - return rankings.EQUAL; + return MATCH_RANK.EQUAL; } // starts with if (lowercaseTestString.startsWith(lowercaseStringToRank)) { - return rankings.STARTS_WITH; + return MATCH_RANK.STARTS_WITH; } // word starts with if (lowercaseTestString.includes(` ${lowercaseStringToRank}`)) { - return rankings.WORD_STARTS_WITH; + return MATCH_RANK.WORD_STARTS_WITH; } // contains if (lowercaseTestString.includes(lowercaseStringToRank)) { - return rankings.CONTAINS; + return MATCH_RANK.CONTAINS; } if (lowercaseStringToRank.length === 1) { - return rankings.NO_MATCH; + return MATCH_RANK.NO_MATCH; } // acronym if (getAcronym(lowercaseTestString).includes(lowercaseStringToRank)) { - return rankings.ACRONYM; + return MATCH_RANK.ACRONYM; } // will return a number between rankings.MATCHES and rankings.MATCHES + 1 depending on how close of a match it is. @@ -263,7 +263,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef return { // ends up being duplicate of 'item' in matches but consistent rankedValue: stringItem, - rank: rankings.NO_MATCH, + rank: MATCH_RANK.NO_MATCH, keyIndex: -1, keyThreshold: options.threshold, }; @@ -285,7 +285,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef }, { rankedValue: item as unknown as string, - rank: rankings.NO_MATCH as Ranking, + rank: MATCH_RANK.NO_MATCH as Ranking, keyIndex: -1, keyThreshold: options.threshold, }, @@ -300,7 +300,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef * @returns the new filtered array */ function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options = {}): T[] { - const {keys, threshold = rankings.MATCHES, strict = false} = options; + const {keys, threshold = MATCH_RANK.MATCHES} = options; function reduceItemsToRanked(matches: Array>, item: T, index: number): Array> { const rankingInfo = getHighestRanking(item, keys, searchValue, options); @@ -314,14 +314,12 @@ function filterArrayByMatch(items: readonly T[], searchValue: string let matchedItems = items.reduce(reduceItemsToRanked, []); - if (strict) { - matchedItems = matchedItems.filter((item) => item.rank >= threshold + 1); - } + matchedItems = matchedItems.filter((item) => item.rank >= threshold + 1); return matchedItems.map((item) => item.item); } export default filterArrayByMatch; -export {rankings}; +export {MATCH_RANK}; export type {Options, KeyAttributesOptions, KeyOption, RankingInfo, ValueGetterKey}; From dea2fb374eae8b2bb07f36604729b6087f38bb80 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 11:27:03 +0200 Subject: [PATCH 27/40] move getAcronym to string utils --- src/libs/StringUtils.ts | 19 ++++++++++++++++++- src/libs/filterArrayByMatch.ts | 20 ++------------------ tests/unit/StringUtilsTest.ts | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 tests/unit/StringUtilsTest.ts diff --git a/src/libs/StringUtils.ts b/src/libs/StringUtils.ts index 2fb918c7a233..94cd04046ccc 100644 --- a/src/libs/StringUtils.ts +++ b/src/libs/StringUtils.ts @@ -72,4 +72,21 @@ function normalizeCRLF(value?: string): string | undefined { return value?.replace(/\r\n/g, '\n'); } -export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF}; +/** + * Generates an acronym for a string. + * @param string the string for which to produce the acronym + * @returns the acronym + */ +function getAcronym(string: string): string { + let acronym = ''; + const wordsInString = string.split(' '); + wordsInString.forEach((wordInString) => { + const splitByHyphenWords = wordInString.split('-'); + splitByHyphenWords.forEach((splitByHyphenWord) => { + acronym += splitByHyphenWord.substring(0, 1); + }); + }); + return acronym; +} + +export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF, getAcronym}; diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index afba4e368aed..8e49fcb40bed 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -3,6 +3,7 @@ Use `threshold` option with one of the rankings defined below to control the strictness of the match. */ import type {ValueOf} from 'type-fest'; +import StringUtils from './StringUtils'; const MATCH_RANK = { CASE_SENSITIVE_EQUAL: 7, @@ -45,23 +46,6 @@ type Options = { }; type IndexableByString = Record; -/** - * Generates an acronym for a string. - * @param string the string for which to produce the acronym - * @returns the acronym - */ -function getAcronym(string: string): string { - let acronym = ''; - const wordsInString = string.split(' '); - wordsInString.forEach((wordInString) => { - const splitByHyphenWords = wordInString.split('-'); - splitByHyphenWords.forEach((splitByHyphenWord) => { - acronym += splitByHyphenWord.substring(0, 1); - }); - }); - return acronym; -} - /** * Given path: "foo.bar.baz" and item: {foo: {bar: {baz: 'buzz'}}} -> 'buzz' * @param path a dot-separated set of keys @@ -240,7 +224,7 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { } // acronym - if (getAcronym(lowercaseTestString).includes(lowercaseStringToRank)) { + if (StringUtils.getAcronym(lowercaseTestString).includes(lowercaseStringToRank)) { return MATCH_RANK.ACRONYM; } diff --git a/tests/unit/StringUtilsTest.ts b/tests/unit/StringUtilsTest.ts new file mode 100644 index 000000000000..04ce748c984b --- /dev/null +++ b/tests/unit/StringUtilsTest.ts @@ -0,0 +1,20 @@ +import StringUtils from '@libs/StringUtils'; + +describe('StringUtils', () => { + describe('getAcronym', () => { + it('should return the acronym of a string with multiple words', () => { + const acronym = StringUtils.getAcronym('Hello World'); + expect(acronym).toBe('HW'); + }); + + it('should return an acronym of a string with a single word', () => { + const acronym = StringUtils.getAcronym('Hello'); + expect(acronym).toBe('H'); + }); + + it('should return an acronym of a string when word in a string has a hyphen', () => { + const acronym = StringUtils.getAcronym('Hello Every-One'); + expect(acronym).toBe('HEO'); + }); + }); +}); From 528ff86a22451df44954f0615969acdaed782657 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 12:04:16 +0200 Subject: [PATCH 28/40] remove createFilter function --- src/libs/OptionsListUtils.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a1dc873440fd..2f42e532f13f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -40,7 +40,6 @@ import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import filterArrayByMatch from './filterArrayByMatch'; -import type {KeyOption} from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -2039,11 +2038,6 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; - const createFilter = (items: ReportUtils.OptionData[], keys: ReadonlyArray>, term: string) => - filterArrayByMatch(items, term, { - keys, - }); - // The regex below is used to remove dots only from the local part of the user email (local-part@domain) // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) const emailRegex = /\.(?=[^\s@]*@)/g; @@ -2070,9 +2064,8 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption }; const matchResults = searchTerms.reduceRight((items, term) => { - const recentReports = createFilter( - items.recentReports ?? [], - [ + const recentReports = filterArrayByMatch(items.recentReports, term, { + keys: [ (item) => { let keys: string[] = []; @@ -2099,13 +2092,14 @@ function filterOptions(options: GetOptions, searchInputValue: string): GetOption return uniqFast(keys); }, ], - term, - ); - const personalDetails = createFilter(items.personalDetails ?? [], ['participantsList.0.displayName', 'login', (item) => item.login?.replace(emailRegex, '') ?? ''], term); + }); + const personalDetails = filterArrayByMatch(items.personalDetails, term, { + keys: ['participantsList.0.displayName', 'login', (item) => item.login?.replace(emailRegex, '') ?? ''], + }); return { - recentReports: recentReports ?? ([] as ReportUtils.OptionData[]), - personalDetails: personalDetails ?? ([] as ReportUtils.OptionData[]), + recentReports: recentReports ?? [], + personalDetails: personalDetails ?? [], userToInvite: null, currentUserOption: null, categoryOptions: [], From b0bd01d2c64b918e6e8eb37a240392adfbfef76e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 13:18:20 +0200 Subject: [PATCH 29/40] update reduce with array function --- src/libs/filterArrayByMatch.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index 8e49fcb40bed..37dced7cf05a 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -286,7 +286,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options = {}): T[] { const {keys, threshold = MATCH_RANK.MATCHES} = options; - function reduceItemsToRanked(matches: Array>, item: T, index: number): Array> { + let matchedItems = items.reduce((matches: Array>, item: T, index: number): Array> => { const rankingInfo = getHighestRanking(item, keys, searchValue, options); const {rank, keyThreshold = threshold} = rankingInfo; @@ -294,9 +294,7 @@ function filterArrayByMatch(items: readonly T[], searchValue: string matches.push({...rankingInfo, item, index}); } return matches; - } - - let matchedItems = items.reduce(reduceItemsToRanked, []); + }, []); matchedItems = matchedItems.filter((item) => item.rank >= threshold + 1); From eab2574a97cfae588def004faa824d720d2d4777 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 15:59:21 +0200 Subject: [PATCH 30/40] remove getClosenessRanking function --- src/libs/filterArrayByMatch.ts | 86 ++++++++++++++-------------------- 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index 37dced7cf05a..ebd5b239f249 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -130,55 +130,6 @@ function getAllValuesToRank(item: T, keys: ReadonlyArray>) { return allValues; } -/** - * Returns a score based on how spread apart the characters from the stringToRank are within the testString. - * A number close to rankings.MATCHES represents a loose match. A number close to rankings.MATCHES + 1 represents a tighter match. - * @param testString - the string to test against - * @param stringToRank - the string to rank - * @returns the number between rankings.MATCHES and - */ -function getClosenessRanking(testString: string, stringToRank: string): Ranking { - let matchingInOrderCharCount = 0; - let charNumber = 0; - function findMatchingCharacter(matchChar: string, string: string, index: number) { - for (let j = index; j < string.length; j++) { - if (string[j] === matchChar) { - matchingInOrderCharCount += 1; - return j + 1; - } - } - return -1; - } - - function getRanking(spread: number) { - const spreadPercentage = 1 / spread; - const inOrderPercentage = matchingInOrderCharCount / stringToRank.length; - const ranking = MATCH_RANK.MATCHES + inOrderPercentage * spreadPercentage; - - return ranking as Ranking; - } - - const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0); - - if (firstIndex < 0) { - return MATCH_RANK.NO_MATCH; - } - - charNumber = firstIndex; - for (let i = 1; i < stringToRank.length; i++) { - const matchChar = stringToRank[i]; - charNumber = findMatchingCharacter(matchChar, testString, charNumber); - const found = charNumber > -1; - - if (!found) { - return MATCH_RANK.NO_MATCH; - } - } - - const spread = charNumber - firstIndex; - return getRanking(spread); -} - /** * Gives a rankings score based on how well the two strings match. * @param testString - the string to test against @@ -229,7 +180,42 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { } // will return a number between rankings.MATCHES and rankings.MATCHES + 1 depending on how close of a match it is. - return getClosenessRanking(lowercaseTestString, lowercaseStringToRank); + let matchingInOrderCharCount = 0; + let charNumber = 0; + function findMatchingCharacter(matchChar: string, string: string, index: number) { + for (let j = index; j < string.length; j++) { + if (string[j] === matchChar) { + matchingInOrderCharCount += 1; + return j + 1; + } + } + return -1; + } + + const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0); + + if (firstIndex < 0) { + return MATCH_RANK.NO_MATCH; + } + + charNumber = firstIndex; + for (let i = 1; i < stringToRank.length; i++) { + const matchChar = stringToRank[i]; + charNumber = findMatchingCharacter(matchChar, testString, charNumber); + const found = charNumber > -1; + + if (!found) { + return MATCH_RANK.NO_MATCH; + } + } + + const spread = charNumber - firstIndex; + + const spreadPercentage = 1 / spread; + const inOrderPercentage = matchingInOrderCharCount / stringToRank.length; + const ranking = MATCH_RANK.MATCHES + inOrderPercentage * spreadPercentage; + + return ranking as Ranking; } /** From 765ae5230fa18fd43eaacda22a7bbe215b6a3a52 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 16:15:09 +0200 Subject: [PATCH 31/40] remove keys check --- src/libs/filterArrayByMatch.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index ebd5b239f249..dd5a093d4547 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -41,7 +41,7 @@ type KeyAttributesOptions = { type KeyOption = KeyAttributesOptions | ValueGetterKey | string; type Options = { - keys?: ReadonlyArray>; + keys: ReadonlyArray>; threshold?: Ranking; }; type IndexableByString = Record; @@ -226,18 +226,7 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { * @param options - options to control the ranking * @returns the highest ranking */ -function getHighestRanking(item: T, keys: ReadonlyArray> | undefined, value: string, options: Options): RankingInfo { - if (!keys) { - // if keys is not specified, then we assume the item given is ready to be matched - const stringItem = item as unknown as string; - return { - // ends up being duplicate of 'item' in matches but consistent - rankedValue: stringItem, - rank: MATCH_RANK.NO_MATCH, - keyIndex: -1, - keyThreshold: options.threshold, - }; - } +function getHighestRanking(item: T, keys: ReadonlyArray>, value: string, options: Options): RankingInfo { const valuesToRank = getAllValuesToRank(item, keys); return valuesToRank.reduce( (acc, itemValue, index) => { @@ -269,7 +258,7 @@ function getHighestRanking(item: T, keys: ReadonlyArray> | undef * @param options - options to configure * @returns the new filtered array */ -function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options = {}): T[] { +function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options): T[] { const {keys, threshold = MATCH_RANK.MATCHES} = options; let matchedItems = items.reduce((matches: Array>, item: T, index: number): Array> => { From f9620929eaf7a1cbf58a39d13148d01296ef7cd0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 3 Apr 2024 16:33:59 +0200 Subject: [PATCH 32/40] update tests --- tests/unit/OptionsListUtilsTest.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index bb0d1fe80010..825341ff7e04 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -2455,7 +2455,7 @@ describe('OptionsListUtils', () => { describe('filterOptions', () => { it('should return all options when search is empty', () => { - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, ''); expect(options.recentReports.length + options.personalDetails.length).toBe(filteredOptions.recentReports.length); @@ -2463,7 +2463,7 @@ describe('OptionsListUtils', () => { it('should return filtered options in correct order', () => { const searchText = 'man'; - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(4); @@ -2475,7 +2475,7 @@ describe('OptionsListUtils', () => { it('should filter users by email', () => { const searchText = 'mistersinister@marauders.com'; - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); @@ -2485,7 +2485,7 @@ describe('OptionsListUtils', () => { it('should find archived chats', () => { const searchText = 'Archived'; - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(1); @@ -2494,7 +2494,8 @@ describe('OptionsListUtils', () => { it('should filter options by email if dot is skipped in the email', () => { const searchText = 'barryallen'; - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, '', [CONST.BETAS.ALL]); + const OPTIONS_WITH_PERIODS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS); + const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_PERIODS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); @@ -2504,7 +2505,7 @@ describe('OptionsListUtils', () => { it('should include workspaces in the search results', () => { const searchText = 'avengers'; - const options = OptionsListUtils.getSearchOptions(REPORTS_WITH_WORKSPACE_ROOMS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACES, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); @@ -2514,7 +2515,7 @@ describe('OptionsListUtils', () => { it('should put exact match by login on the top of the list', () => { const searchText = 'reedrichards@expensify.com'; - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); @@ -2524,7 +2525,8 @@ describe('OptionsListUtils', () => { it('should prioritize options with matching display name over chatrooms', () => { const searchText = 'spider'; - const options = OptionsListUtils.getSearchOptions(REPORTS_WITH_CHAT_ROOM, PERSONAL_DETAILS, '', [CONST.BETAS.ALL]); + const OPTIONS_WITH_CHATROOMS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_CHAT_ROOM); + const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_CHATROOMS, '', [CONST.BETAS.ALL]); const filterOptions = OptionsListUtils.filterOptions(options, searchText); @@ -2535,7 +2537,7 @@ describe('OptionsListUtils', () => { it('should put the item with latest lastVisibleActionCreated on top when search value match multiple items', () => { const searchText = 'fantastic'; - const options = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, ''); + const options = OptionsListUtils.getSearchOptions(OPTIONS, ''); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); expect(filteredOptions.recentReports.length).toBe(2); From e3b3b97ce4ab842fee370790651690a67ddeebed Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 5 Apr 2024 09:29:34 +0200 Subject: [PATCH 33/40] change GetOptions type to Options --- src/libs/OptionsListUtils.ts | 14 +++++++------- src/pages/SearchPage/index.tsx | 2 +- .../AboutPage/ShareLogList/BaseShareLogList.tsx | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f51096f21855..13964de0d37a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -161,7 +161,7 @@ type MemberForList = { type SectionForSearchTerm = { section: CategorySection; }; -type GetOptions = { +type Options = { recentReports: ReportUtils.OptionData[]; personalDetails: ReportUtils.OptionData[]; userToInvite: ReportUtils.OptionData | null; @@ -1471,7 +1471,7 @@ function getOptions( taxRates, includeSelfDM = false, }: GetOptionsConfig, -): GetOptions { +): Options { if (includeCategories) { const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); @@ -1782,7 +1782,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); const optionList = getOptions(options, { @@ -1807,7 +1807,7 @@ function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = return optionList; } -function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { +function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options { return getOptions(options, { betas, searchInputValue: searchValue.trim(), @@ -1978,7 +1978,7 @@ function getMemberInviteOptions( searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false, -): GetOptions { +): Options { return getOptions( {reports: [], personalDetails}, { @@ -2100,7 +2100,7 @@ function formatSectionsFromSearchTerm( /** * Filters options based on the search input value */ -function filterOptions(options: GetOptions, searchInputValue: string): GetOptions { +function filterOptions(options: Options, searchInputValue: string): Options { const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; @@ -2226,4 +2226,4 @@ export { getTaxRatesSection, }; -export type {MemberForList, CategorySection, GetOptions, OptionList, SearchOption, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, Options, OptionList, SearchOption, PayeePersonalDetails, Category}; diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index d9c187f0adc0..fe0daa9cf40d 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -37,7 +37,7 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; -type Options = OptionsListUtils.GetOptions & {headerMessage: string}; +type Options = OptionsListUtils.Options & {headerMessage: string}; type SearchPageSectionItem = { data: OptionData[]; diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index cee62380a011..994232470520 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -18,7 +18,7 @@ import type {BaseShareLogListOnyxProps, BaseShareLogListProps} from './types'; function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { const [searchValue, setSearchValue] = useState(''); - const [searchOptions, setSearchOptions] = useState>({ + const [searchOptions, setSearchOptions] = useState>({ recentReports: [], personalDetails: [], userToInvite: null, From 5ea71db886f03c0d6b38799d2af2b32665676c80 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 5 Apr 2024 09:59:16 +0200 Subject: [PATCH 34/40] remove dot-notation pattern --- src/libs/OptionsListUtils.ts | 2 +- src/libs/filterArrayByMatch.ts | 38 ---------------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 13964de0d37a..35d33ef2033b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2160,7 +2160,7 @@ function filterOptions(options: Options, searchInputValue: string): Options { ], }); const personalDetails = filterArrayByMatch(items.personalDetails, term, { - keys: ['participantsList.0.displayName', 'login', (item) => item.login?.replace(emailRegex, '') ?? ''], + keys: [(item) => item.participantsList?.[0]?.displayName ?? '', 'login', (item) => item.login?.replace(emailRegex, '') ?? ''], }); return { diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index dd5a093d4547..ba74b2f76376 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -46,42 +46,6 @@ type Options = { }; type IndexableByString = Record; -/** - * Given path: "foo.bar.baz" and item: {foo: {bar: {baz: 'buzz'}}} -> 'buzz' - * @param path a dot-separated set of keys - * @param item the item to get the value from - */ -function getNestedValues(path: string, item: T): string[] { - const keys = path.split('.'); - let values: Array = [item]; - - for (const nestedKey of keys) { - let nestedValues: Array = []; - - for (const nestedItem of values) { - if (nestedItem != null) { - if (Object.hasOwnProperty.call(nestedItem, nestedKey)) { - const nestedValue = (nestedItem as IndexableByString)[nestedKey]; - if (nestedValue != null) { - nestedValues.push(nestedValue as IndexableByString | string); - } - } else if (nestedKey === '*') { - nestedValues = nestedValues.concat(nestedItem); - } - } - } - - values = nestedValues; - } - - if (Array.isArray(values[0])) { - const result: string[] = []; - return result.concat(...(values as string[])); - } - - return values as string[]; -} - /** * Gets value for key in item at arbitrarily nested keypath * @param item - the item @@ -101,8 +65,6 @@ function getItemValues(item: T, key: KeyOption): string[] { value = null; } else if (Object.hasOwnProperty.call(item, keyCopy)) { value = (item as IndexableByString)[keyCopy]; - } else if (keyCopy.includes('.')) { - return getNestedValues(keyCopy, item); } else { return []; } From fae57e2268aae9a52be43ccdaf908345892d937f Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 5 Apr 2024 14:04:35 +0200 Subject: [PATCH 35/40] simplify the implementation --- src/libs/filterArrayByMatch.ts | 91 ++++++++-------------------------- 1 file changed, 21 insertions(+), 70 deletions(-) diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index ba74b2f76376..f481a08fe0b3 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -27,14 +27,8 @@ type RankingInfo = { type ValueGetterKey = (item: T) => string | string[]; -type IndexedItem = { - item: T; - index: number; -}; -type RankedItem = RankingInfo & IndexedItem; - type KeyAttributesOptions = { - key?: string | ValueGetterKey; + key: string | ValueGetterKey; threshold?: Ranking; }; @@ -53,26 +47,18 @@ type IndexableByString = Record; * @returns an array containing the value(s) at the nested keypath */ function getItemValues(item: T, key: KeyOption): string[] { - let keyCopy = key; - if (typeof keyCopy === 'object') { - keyCopy = keyCopy.key as string; - } - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - let value: string | string[] | null | unknown; - if (typeof keyCopy === 'function') { - value = keyCopy(item); - } else if (item == null) { - value = null; - } else if (Object.hasOwnProperty.call(item, keyCopy)) { - value = (item as IndexableByString)[keyCopy]; - } else { + if (!item) { return []; } - if (Array.isArray(value)) { - return value as string[]; + const resolvedKey = typeof key === 'object' ? key.key : key; + const value = typeof resolvedKey === 'function' ? resolvedKey(item) : (item as IndexableByString)[resolvedKey]; + + if (!value) { + return []; } - return [String(value)]; + + return Array.isArray(value) ? value.map(String) : [String(value)]; } /** @@ -81,15 +67,8 @@ function getItemValues(item: T, key: KeyOption): string[] { * @param keys - the keys to use to retrieve the values * @return objects with {itemValue} */ -function getAllValuesToRank(item: T, keys: ReadonlyArray>) { - const allValues: string[] = []; - for (const key of keys) { - const itemValues = getItemValues(item, key); - for (const value of itemValues) { - allValues.push(value); - } - } - return allValues; +function getAllValuesToRank(item: T, keys: ReadonlyArray>): string[] { + return keys.flatMap((key) => getItemValues(item, key)); } /** @@ -144,35 +123,16 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { // will return a number between rankings.MATCHES and rankings.MATCHES + 1 depending on how close of a match it is. let matchingInOrderCharCount = 0; let charNumber = 0; - function findMatchingCharacter(matchChar: string, string: string, index: number) { - for (let j = index; j < string.length; j++) { - if (string[j] === matchChar) { - matchingInOrderCharCount += 1; - return j + 1; - } - } - return -1; - } - - const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0); - - if (firstIndex < 0) { - return MATCH_RANK.NO_MATCH; - } - - charNumber = firstIndex; - for (let i = 1; i < stringToRank.length; i++) { - const matchChar = stringToRank[i]; - charNumber = findMatchingCharacter(matchChar, testString, charNumber); - const found = charNumber > -1; - - if (!found) { + for (const char of stringToRank) { + charNumber = lowercaseTestString.indexOf(char, charNumber) + 1; + if (!charNumber) { return MATCH_RANK.NO_MATCH; } + matchingInOrderCharCount++; } - const spread = charNumber - firstIndex; - + // Calculate ranking based on character sequence and spread + const spread = charNumber - lowercaseTestString.indexOf(stringToRank[0]); const spreadPercentage = 1 / spread; const inOrderPercentage = matchingInOrderCharCount / stringToRank.length; const ranking = MATCH_RANK.MATCHES + inOrderPercentage * spreadPercentage; @@ -223,19 +183,10 @@ function getHighestRanking(item: T, keys: ReadonlyArray>, value: function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options): T[] { const {keys, threshold = MATCH_RANK.MATCHES} = options; - let matchedItems = items.reduce((matches: Array>, item: T, index: number): Array> => { - const rankingInfo = getHighestRanking(item, keys, searchValue, options); - const {rank, keyThreshold = threshold} = rankingInfo; - - if (rank >= keyThreshold) { - matches.push({...rankingInfo, item, index}); - } - return matches; - }, []); - - matchedItems = matchedItems.filter((item) => item.rank >= threshold + 1); - - return matchedItems.map((item) => item.item); + return items + .map((item) => ({...getHighestRanking(item, keys, searchValue, options), item})) + .filter(({rank, keyThreshold = threshold}) => rank >= Math.max(keyThreshold, threshold + 1)) + .map(({item}) => item); } export default filterArrayByMatch; From 0682936b7550be7fdc8ce4c511151b9f71fbf9e7 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 9 Apr 2024 08:41:04 +0200 Subject: [PATCH 36/40] fix typecheck --- tests/unit/OptionsListUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 1a4f07ef7f7b..3ba6f1b79463 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -268,7 +268,7 @@ describe('OptionsListUtils', () => { lastReadTime: '2021-01-14 11:25:39.301', lastVisibleActionCreated: '2022-11-22 03:26:02.000', isPinned: false, - reportID: 15, + reportID: '15', participantAccountIDs: [3, 4], visibleChatMemberAccountIDs: [3, 4], reportName: 'Spider-Man, Black Panther', From efedd80ee8342ddbcacd853403ad993c3c4ab791 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 9 Apr 2024 12:46:38 +0200 Subject: [PATCH 37/40] remove debounced state dependency --- src/libs/OptionsListUtils.ts | 6 +++--- src/pages/SearchPage/index.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 06da1af1d6e9..33820da83471 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2245,14 +2245,14 @@ function filterOptions(options: Options, searchInputValue: string): Options { return keys; }; - const matchResults = searchTerms.reduceRight((items, term) => { const recentReports = filterArrayByMatch(items.recentReports, term, { keys: [ (item) => { let keys: string[] = []; - - keys.push(item.text ?? ''); + if (item.text) { + keys.push(item.text); + } if (item.login) { keys.push(item.login); diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index fe0daa9cf40d..7d2a5bfecbb8 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -88,10 +88,10 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) headerMessage: '', }; } - const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); - const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); + const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); + const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), ''); return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, debouncedSearchValue, options]); + }, [areOptionsInitialized, betas, options]); const filteredOptions = useMemo(() => { if (debouncedSearchValue.trim() === '') { From f94881e3e86761e94ae4f42595c72bcb95d7c6e9 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 10 Apr 2024 12:11:42 +0200 Subject: [PATCH 38/40] update filtering method --- src/libs/OptionsListUtils.ts | 54 ++++++++-------- src/libs/filterArrayByMatch.ts | 112 +++++---------------------------- 2 files changed, 42 insertions(+), 124 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 58a3cf50ae1f..eac5548921e4 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2247,39 +2247,35 @@ function filterOptions(options: Options, searchInputValue: string): Options { return keys; }; const matchResults = searchTerms.reduceRight((items, term) => { - const recentReports = filterArrayByMatch(items.recentReports, term, { - keys: [ - (item) => { - let keys: string[] = []; - if (item.text) { - keys.push(item.text); - } + const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { + let values: string[] = []; + if (item.text) { + values.push(item.text); + } - if (item.login) { - keys.push(item.login); - keys.push(item.login.replace(emailRegex, '')); - } + if (item.login) { + values.push(item.login); + values.push(item.login.replace(emailRegex, '')); + } - if (item.isThread) { - if (item.alternateText) { - keys.push(item.alternateText); - } - keys = keys.concat(getParticipantsLoginsArray(item)); - } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { - if (item.subtitle) { - keys.push(item.subtitle); - } - } else { - keys = keys.concat(getParticipantsLoginsArray(item)); - } + if (item.isThread) { + if (item.alternateText) { + values.push(item.alternateText); + } + values = values.concat(getParticipantsLoginsArray(item)); + } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { + if (item.subtitle) { + values.push(item.subtitle); + } + } else { + values = values.concat(getParticipantsLoginsArray(item)); + } - return uniqFast(keys); - }, - ], - }); - const personalDetails = filterArrayByMatch(items.personalDetails, term, { - keys: [(item) => item.participantsList?.[0]?.displayName ?? '', 'login', (item) => item.login?.replace(emailRegex, '') ?? ''], + return uniqFast(values); }); + const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => + uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login?.replace(emailRegex, '') ?? '']), + ); return { recentReports: recentReports ?? [], diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index f481a08fe0b3..c87ad8fac180 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -18,59 +18,6 @@ const MATCH_RANK = { type Ranking = ValueOf; -type RankingInfo = { - rankedValue: string; - rank: Ranking; - keyIndex: number; - keyThreshold?: Ranking; -}; - -type ValueGetterKey = (item: T) => string | string[]; - -type KeyAttributesOptions = { - key: string | ValueGetterKey; - threshold?: Ranking; -}; - -type KeyOption = KeyAttributesOptions | ValueGetterKey | string; - -type Options = { - keys: ReadonlyArray>; - threshold?: Ranking; -}; -type IndexableByString = Record; - -/** - * Gets value for key in item at arbitrarily nested keypath - * @param item - the item - * @param key - the potentially nested keypath or property callback - * @returns an array containing the value(s) at the nested keypath - */ -function getItemValues(item: T, key: KeyOption): string[] { - if (!item) { - return []; - } - - const resolvedKey = typeof key === 'object' ? key.key : key; - const value = typeof resolvedKey === 'function' ? resolvedKey(item) : (item as IndexableByString)[resolvedKey]; - - if (!value) { - return []; - } - - return Array.isArray(value) ? value.map(String) : [String(value)]; -} - -/** - * Gets all the values for the given keys in the given item and returns an array of those values - * @param item - the item from which the values will be retrieved - * @param keys - the keys to use to retrieve the values - * @return objects with {itemValue} - */ -function getAllValuesToRank(item: T, keys: ReadonlyArray>): string[] { - return keys.flatMap((key) => getItemValues(item, key)); -} - /** * Gives a rankings score based on how well the two strings match. * @param testString - the string to test against @@ -140,56 +87,31 @@ function getMatchRanking(testString: string, stringToRank: string): Ranking { return ranking as Ranking; } -/** - * Gets the highest ranking for value for the given item based on its values for the given keys - * @param item - the item to rank - * @param keys - the keys to get values from the item for the ranking - * @param value - the value to rank against - * @param options - options to control the ranking - * @returns the highest ranking - */ -function getHighestRanking(item: T, keys: ReadonlyArray>, value: string, options: Options): RankingInfo { - const valuesToRank = getAllValuesToRank(item, keys); - return valuesToRank.reduce( - (acc, itemValue, index) => { - const ranking = acc; - const newRank = getMatchRanking(itemValue, value); - let newRankedValue = ranking.rankedValue; - - if (newRank > ranking.rank) { - ranking.rank = newRank; - ranking.keyIndex = index; - ranking.keyThreshold = options.threshold; - newRankedValue = itemValue; - } - return {rankedValue: newRankedValue, rank: ranking.rank, keyIndex: ranking.keyIndex, keyThreshold: ranking.keyThreshold}; - }, - { - rankedValue: item as unknown as string, - rank: MATCH_RANK.NO_MATCH as Ranking, - keyIndex: -1, - keyThreshold: options.threshold, - }, - ); -} - /** * Takes an array of items and a value and returns a new array with the items that match the given value * @param items - the items to filter * @param searchValue - the value to use for ranking - * @param options - options to configure + * @param extractRankableValuesFromItem - an array of functions * @returns the new filtered array */ -function filterArrayByMatch(items: readonly T[], searchValue: string, options: Options): T[] { - const {keys, threshold = MATCH_RANK.MATCHES} = options; +function filterArrayByMatch(items: readonly T[], searchValue: string, extractRankableValuesFromItem: (item: T) => string[]): T[] { + const filteredItems = []; + for (const item of items) { + const valuesToRank = extractRankableValuesFromItem(item); + let itemRank: Ranking = MATCH_RANK.NO_MATCH; + for (const value of valuesToRank) { + const rank = getMatchRanking(value, searchValue); + if (rank > itemRank) { + itemRank = rank; + } + } - return items - .map((item) => ({...getHighestRanking(item, keys, searchValue, options), item})) - .filter(({rank, keyThreshold = threshold}) => rank >= Math.max(keyThreshold, threshold + 1)) - .map(({item}) => item); + if (itemRank >= MATCH_RANK.MATCHES) { + filteredItems.push(item); + } + } + return filteredItems; } export default filterArrayByMatch; export {MATCH_RANK}; - -export type {Options, KeyAttributesOptions, KeyOption, RankingInfo, ValueGetterKey}; From 7620b3591d9c10776dfe2d50e9cffe9a9cb8f262 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 10 Apr 2024 12:35:10 +0200 Subject: [PATCH 39/40] show chatrooms where user is a participant --- src/libs/OptionsListUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index eac5548921e4..39d583106d51 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2267,6 +2267,7 @@ function filterOptions(options: Options, searchInputValue: string): Options { if (item.subtitle) { values.push(item.subtitle); } + values = values.concat(getParticipantsLoginsArray(item)); } else { values = values.concat(getParticipantsLoginsArray(item)); } From 5b42f38334793bffeceb7c8bd14dc8c325fce827 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 10 Apr 2024 14:40:37 +0200 Subject: [PATCH 40/40] fix tests --- src/libs/OptionsListUtils.ts | 7 ++----- src/libs/filterArrayByMatch.ts | 2 +- tests/unit/OptionsListUtilsTest.ts | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 39d583106d51..d63b3384c933 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2262,20 +2262,17 @@ function filterOptions(options: Options, searchInputValue: string): Options { if (item.alternateText) { values.push(item.alternateText); } - values = values.concat(getParticipantsLoginsArray(item)); } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { if (item.subtitle) { values.push(item.subtitle); } - values = values.concat(getParticipantsLoginsArray(item)); - } else { - values = values.concat(getParticipantsLoginsArray(item)); } + values = values.concat(getParticipantsLoginsArray(item)); return uniqFast(values); }); const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => - uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login?.replace(emailRegex, '') ?? '']), + uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(emailRegex, '') ?? '']), ); return { diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts index c87ad8fac180..3abf82b6afab 100644 --- a/src/libs/filterArrayByMatch.ts +++ b/src/libs/filterArrayByMatch.ts @@ -106,7 +106,7 @@ function filterArrayByMatch(items: readonly T[], searchValue: string } } - if (itemRank >= MATCH_RANK.MATCHES) { + if (itemRank >= MATCH_RANK.MATCHES + 1) { filteredItems.push(item); } } diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index fa0175ffa8f3..af5782b1ca32 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -2609,11 +2609,12 @@ describe('OptionsListUtils', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText); - expect(filteredOptions.recentReports.length).toBe(4); + expect(filteredOptions.recentReports.length).toBe(5); expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman'); expect(filteredOptions.recentReports[1].text).toBe('Spider-Man'); expect(filteredOptions.recentReports[2].text).toBe('Black Widow'); expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic'); + expect(filteredOptions.recentReports[4].text).toBe("SHIELD's workspace (archived)"); }); it('should filter users by email', () => {