diff --git a/src/CONST.ts b/src/CONST.ts index 6cbd983b92253..9c44e6539071d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6189,6 +6189,7 @@ const CONST = { LOWER_THAN: 'lt', LOWER_THAN_OR_EQUAL_TO: 'lte', }, + SYNTAX_RANGE_NAME: 'syntax', SYNTAX_ROOT_KEYS: { TYPE: 'type', STATUS: 'status', diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 40157e929b33e..a89c68364ad94 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -1,23 +1,27 @@ import type {ForwardedRef, ReactNode, RefObject} from 'react'; -import React, {forwardRef, useLayoutEffect, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import {useSharedValue} from 'react-native-reanimated'; import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {parseFSAttributes} from '@libs/Fullstory'; -import {parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; +import runOnLiveMarkdownRuntime from '@libs/runOnLiveMarkdownRuntime'; +import {getAutocompleteCategories, getAutocompleteTags, parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; import handleKeyPress from '@libs/SearchInputOnKeyPress'; import shouldDelayFocus from '@libs/shouldDelayFocus'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {SubstitutionMap} from './SearchRouter/getQueryWithSubstitutions'; type SearchAutocompleteInputProps = { /** Value of TextInput */ @@ -61,6 +65,9 @@ type SearchAutocompleteInputProps = { /** Whether the search reports API call is running */ isSearchingForReports?: boolean; + + /** Map of autocomplete suggestions. Required for highlighting to work properly */ + substitutionMap: SubstitutionMap; } & Pick; function SearchAutocompleteInput( @@ -82,6 +89,7 @@ function SearchAutocompleteInput( rightComponent, isSearchingForReports, selection, + substitutionMap, }: SearchAutocompleteInputProps, ref: ForwardedRef, ) { @@ -89,13 +97,72 @@ function SearchAutocompleteInput( const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); const {isOffline} = useNetwork(); + const {activeWorkspaceID} = useActiveWorkspace(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); + const currencyAutocompleteList = Object.keys(currencyList ?? {}); + const currencySharedValue = useSharedValue(currencyAutocompleteList); + + const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); + const categoryAutocompleteList = useMemo(() => { + return getAutocompleteCategories(allPolicyCategories, activeWorkspaceID); + }, [activeWorkspaceID, allPolicyCategories]); + const categorySharedValue = useSharedValue(categoryAutocompleteList); + + const [allPoliciesTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); + const tagAutocompleteList = useMemo(() => { + return getAutocompleteTags(allPoliciesTags, activeWorkspaceID); + }, [activeWorkspaceID, allPoliciesTags]); + const tagSharedValue = useSharedValue(tagAutocompleteList); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); const emailList = Object.keys(loginList ?? {}); + const emailListSharedValue = useSharedValue(emailList); const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + emailListSharedValue.set(emailList); + })(); + }, [emailList, emailListSharedValue]); + + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + currencySharedValue.set(currencyAutocompleteList); + })(); + }, [currencyAutocompleteList, currencySharedValue]); + + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + categorySharedValue.set(categoryAutocompleteList); + })(); + }, [categorySharedValue, categoryAutocompleteList]); + + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + tagSharedValue.set(tagAutocompleteList); + }); + }, [tagSharedValue, tagAutocompleteList]); + + const parser = useCallback( + (input: string) => { + 'worklet'; + + return parseForLiveMarkdown(input, currentUserPersonalDetails.displayName ?? '', substitutionMap, emailListSharedValue, currencySharedValue, categorySharedValue, tagSharedValue); + }, + [currentUserPersonalDetails.displayName, substitutionMap, currencySharedValue, categorySharedValue, tagSharedValue, emailListSharedValue], + ); + const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; // Parse Fullstory attributes on initial render @@ -145,11 +212,7 @@ function SearchAutocompleteInput( onKeyPress={handleKeyPress(onSubmit)} type="markdown" multiline={false} - parser={(input: string) => { - 'worklet'; - - return parseForLiveMarkdown(input, emailList, currentUserPersonalDetails.displayName ?? ''); - }} + parser={parser} selection={selection} /> diff --git a/src/components/Search/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeaderInput.tsx index a3e6e0a034fda..d39c58f9edea5 100644 --- a/src/components/Search/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeaderInput.tsx @@ -1,4 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; +import isEqual from 'lodash/isEqual'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -123,7 +124,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps setAutocompleteQueryValue(updatedUserQuery); const updatedSubstitutionsMap = getUpdatedSubstitutionsMap(userQuery, autocompleteSubstitutions); - setAutocompleteSubstitutions(updatedSubstitutionsMap); + if (!isEqual(autocompleteSubstitutions, updatedSubstitutionsMap)) { + setAutocompleteSubstitutions(updatedSubstitutionsMap); + } if (updatedUserQuery) { listRef.current?.updateAndScrollToFocusedIndex(0); @@ -290,6 +293,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps autocompleteListRef={listRef} ref={textInputRef} selection={selection} + substitutionMap={autocompleteSubstitutions} /> 0) { listRef.current?.updateAndScrollToFocusedIndex(0); @@ -323,6 +326,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) wrapperFocusedStyle={[styles.borderColorFocus]} isSearchingForReports={isSearchingForReports} selection={selection} + substitutionMap={autocompleteSubstitutions} ref={textInputRef} /> range.key !== CONST.SEARCH.SYNTAX_RANGE_NAME); if (searchAutocompleteQueryRanges.length === 0) { return {}; diff --git a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts index 84895efb33a5b..e20577b7df459 100644 --- a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts +++ b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts @@ -1,10 +1,11 @@ -import type {SearchAutocompleteQueryRange, SearchFilterKey} from '@components/Search/types'; +import type {SearchAutocompleteQueryRange, SearchAutocompleteQueryRangeKey} from '@components/Search/types'; import {parse} from '@libs/SearchParser/autocompleteParser'; import {sanitizeSearchValue} from '@libs/SearchQueryUtils'; +import CONST from '@src/CONST'; type SubstitutionMap = Record; -const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`; +const getSubstitutionMapKey = (filterKey: SearchAutocompleteQueryRangeKey, value: string) => `${filterKey}:${value}`; /** * Given a plaintext query and a SubstitutionMap object, this function will return a transformed query where: @@ -21,7 +22,7 @@ const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${ function getQueryWithSubstitutions(changedQuery: string, substitutions: SubstitutionMap) { const parsed = parse(changedQuery) as {ranges: SearchAutocompleteQueryRange[]}; - const searchAutocompleteQueryRanges = parsed.ranges; + const searchAutocompleteQueryRanges = parsed.ranges.filter((range) => range.key !== CONST.SEARCH.SYNTAX_RANGE_NAME); if (searchAutocompleteQueryRanges.length === 0) { return changedQuery; diff --git a/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts b/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts index ee7bf38502592..aa37f84acc68d 100644 --- a/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts +++ b/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts @@ -1,8 +1,9 @@ -import type {SearchAutocompleteQueryRange, SearchFilterKey} from '@components/Search/types'; -import * as parser from '@libs/SearchParser/autocompleteParser'; +import type {SearchAutocompleteQueryRange, SearchAutocompleteQueryRangeKey} from '@components/Search/types'; +import {parse} from '@libs/SearchParser/autocompleteParser'; +import CONST from '@src/CONST'; import type {SubstitutionMap} from './getQueryWithSubstitutions'; -const getSubstitutionsKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`; +const getSubstitutionsKey = (filterKey: SearchAutocompleteQueryRangeKey, value: string) => `${filterKey}:${value}`; /** * Given a plaintext query and a SubstitutionMap object, @@ -16,9 +17,9 @@ const getSubstitutionsKey = (filterKey: SearchFilterKey, value: string) => `${fi * return: {} */ function getUpdatedSubstitutionsMap(query: string, substitutions: SubstitutionMap): SubstitutionMap { - const parsedQuery = parser.parse(query) as {ranges: SearchAutocompleteQueryRange[]}; + const parsedQuery = parse(query) as {ranges: SearchAutocompleteQueryRange[]}; - const searchAutocompleteQueryRanges = parsedQuery.ranges; + const searchAutocompleteQueryRanges = parsedQuery.ranges.filter((range) => range.key !== CONST.SEARCH.SYNTAX_RANGE_NAME); if (searchAutocompleteQueryRanges.length === 0) { return {}; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 3dc408cc27c30..887c70944660d 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -98,6 +98,8 @@ type SearchFilterKey = | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID; +type SearchAutocompleteQueryRangeKey = SearchFilterKey | typeof CONST.SEARCH.SYNTAX_RANGE_NAME; + type UserFriendlyKey = ValueOf; type QueryFilters = Array<{ @@ -130,7 +132,7 @@ type SearchAutocompleteResult = { }; type SearchAutocompleteQueryRange = { - key: SearchFilterKey; + key: SearchAutocompleteQueryRangeKey; length: number; start: number; value: string; @@ -159,4 +161,5 @@ export type { SearchAutocompleteResult, PaymentData, SearchAutocompleteQueryRange, + SearchAutocompleteQueryRangeKey, }; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 2aece1753758c..365134546c2be 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -1,6 +1,8 @@ import type {MarkdownRange} from '@expensify/react-native-live-markdown'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {SearchAutocompleteResult} from '@components/Search/types'; +import type {SharedValue} from 'react-native-reanimated/lib/typescript/commonTypes'; +import type {SubstitutionMap} from '@components/Search/SearchRouter/getQueryWithSubstitutions'; +import type {SearchAutocompleteQueryRange, SearchAutocompleteResult} from '@components/Search/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags} from '@src/types/onyx'; @@ -133,26 +135,81 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { return newQuery; } +function filterOutRangesWithCorrectValue( + range: SearchAutocompleteQueryRange, + userDisplayName: string, + substitutionMap: SubstitutionMap, + userLogins: SharedValue, + currencyList: SharedValue, + categoryList: SharedValue, + tagList: SharedValue, +) { + 'worklet'; + + const typeList = Object.values(CONST.SEARCH.DATA_TYPES) as string[]; + const expenseTypeList = Object.values(CONST.SEARCH.TRANSACTION_TYPE) as string[]; + const statusList = Object.values({...CONST.SEARCH.STATUS.TRIP, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP}) as string[]; + + switch (range.key) { + case CONST.SEARCH.SYNTAX_FILTER_KEYS.IN: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID: + return substitutionMap[`${range.key}:${range.value}`] !== undefined; + + case CONST.SEARCH.SYNTAX_FILTER_KEYS.TO: + case CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM: + return substitutionMap[`${range.key}:${range.value}`] !== undefined || userLogins.get().includes(range.value); + + case CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY: + return currencyList.get().includes(range.value); + case CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE: + return typeList.includes(range.value); + case CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE: + return expenseTypeList.includes(range.value); + case CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS: + return statusList.includes(range.value); + case CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY: + return categoryList.get().includes(range.value); + case CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG: + return tagList.get().includes(range.value); + default: + return true; + } +} + /** * Parses input string using the autocomplete parser and returns array of * markdown ranges that can be used by RNMarkdownTextInput. * It is simpler version of search parser that can be run on UI. */ -function parseForLiveMarkdown(input: string, userLogins: string[], userDisplayName: string) { +function parseForLiveMarkdown( + input: string, + userDisplayName: string, + map: SubstitutionMap, + userLogins: SharedValue, + currencyList: SharedValue, + categoryList: SharedValue, + tagList: SharedValue, +) { 'worklet'; const parsedAutocomplete = parse(input) as SearchAutocompleteResult; const ranges = parsedAutocomplete.ranges; + return ranges + .filter((range) => filterOutRangesWithCorrectValue(range, userDisplayName, map, userLogins, currencyList, categoryList, tagList)) + .map((range) => { + let type = 'mention-user'; - return ranges.map((range) => { - let type = 'mention-user'; + if (range.key === CONST.SEARCH.SYNTAX_RANGE_NAME) { + type = CONST.SEARCH.SYNTAX_RANGE_NAME; + } - if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (userLogins.includes(range.value) || range.value === userDisplayName)) { - type = 'mention-here'; - } + if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (userLogins.get().includes(range.value) || range.value === userDisplayName)) { + type = 'mention-here'; + } - return {...range, type}; - }) as MarkdownRange[]; + return {...range, type}; + }) as MarkdownRange[]; } export { diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index f4ebc66f828e4..5ef8d782c839e 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -283,24 +283,38 @@ function peg$parse(input, options) { if (!value) { autocomplete = { key, - value: '', + value: "", start: location().end.offset, length: 0, }; - return; + return { + key: "syntax", + value: key, + start: location().start.offset, + length: location().end.offset - location().start.offset, + }; } autocomplete = { key, ...value[value.length - 1], }; - - return value + const result = value .filter((filter) => filter.length > 0) .map((filter) => ({ key, ...filter, })); + + return [ + { + key: "syntax", + value: key, + start: location().start.offset, + length: result[0].start - location().start.offset, + }, + ...result, + ]; }; var peg$f3 = function() { autocomplete = null; }; var peg$f4 = function(parts, empty) { diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 54af9dcf9e816..96bbe4dcc0ff8 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -32,27 +32,41 @@ defaultFilter if (!value) { autocomplete = { key, - value: '', + value: "", start: location().end.offset, length: 0, }; - return; + return { + key: "syntax", + value: key, + start: location().start.offset, + length: location().end.offset - location().start.offset, + }; } autocomplete = { key, ...value[value.length - 1], }; - - return value + const result = value .filter((filter) => filter.length > 0) .map((filter) => ({ key, ...filter, })); + + return [ + { + key: "syntax", + value: key, + start: location().start.offset, + length: result[0].start - location().start.offset, + }, + ...result, + ]; } -freeTextFilter = _ (identifier/ ",") _ { autocomplete = null; } +freeTextFilter = _ (identifier / ",") _ { autocomplete = null; } autocompleteKey "key" = @( diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 463fe85566a6c..66846116ca357 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -222,7 +222,7 @@ function peg$parse(input, options) { var peg$c35 = "<="; var peg$c36 = "<"; - var peg$r0 = /^[^ \t\r\n]/; + var peg$r0 = /^[^ \t\r\n\xA0]/; var peg$r1 = /^[:=]/; var peg$r2 = /^[^ ,"\u201D\u201C\t\n\r\xA0]/; var peg$r3 = /^["\u201C-\u201D]/; @@ -230,7 +230,7 @@ function peg$parse(input, options) { var peg$r5 = /^[^ ,\t\n\r\xA0]/; var peg$r6 = /^[ \t\r\n\xA0]/; - var peg$e0 = peg$classExpectation([" ", "\t", "\r", "\n"], true, false); + var peg$e0 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], true, false); var peg$e1 = peg$otherExpectation("key"); var peg$e2 = peg$otherExpectation("default key"); var peg$e3 = peg$literalExpectation(",", false); @@ -302,7 +302,9 @@ function peg$parse(input, options) { const keywordFilter = buildFilter( "eq", "keyword", - keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat() + keywords + .map((filter) => filter.right.replace(/^(['"])(.*)\1$/, "$2")) + .flat() ); if (keywordFilter.right.length > 0) { nonKeywords.push(keywordFilter); @@ -315,10 +317,14 @@ function peg$parse(input, options) { updateDefaultValues(key, value); }; var peg$f3 = function(value) { + //handle no-breaking space + let word; if (Array.isArray(value)) { - return buildFilter("eq", "keyword", value.join("")); + word = value.join(""); + } else { + word = value; } - return buildFilter("eq", "keyword", value); + return buildFilter("eq", "keyword", word); }; var peg$f4 = function(field, op, values) { return buildFilter(op, field, values); diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index b379f22b59e10..cfbf076e381c6 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -70,7 +70,9 @@ filterList const keywordFilter = buildFilter( "eq", "keyword", - keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat() + keywords + .map((filter) => filter.right.replace(/^(['"])(.*)\1$/, "$2")) + .flat() ); if (keywordFilter.right.length > 0) { nonKeywords.push(keywordFilter); @@ -88,11 +90,15 @@ defaultFilter } freeTextFilter - = _ value:(quotedString / [^ \t\r\n]+) _ { + = _ value:(quotedString / [^ \t\r\n\xA0]+) _ { + //handle no-breaking space + let word; if (Array.isArray(value)) { - return buildFilter("eq", "keyword", value.join("")); + word = value.join(""); + } else { + word = value; } - return buildFilter("eq", "keyword", value); + return buildFilter("eq", "keyword", word); } standardFilter @@ -124,8 +130,7 @@ key "key" / posted ) -defaultKey "default key" - = @(type / status / sortBy / sortOrder / policyID) +defaultKey "default key" = @(type / status / sortBy / sortOrder / policyID) identifier = (","+)? parts:(quotedString / alphanumeric)|1.., ","+| empty:(","+)? { diff --git a/src/libs/runOnLiveMarkdownRuntime/index.native.tsx b/src/libs/runOnLiveMarkdownRuntime/index.native.tsx new file mode 100644 index 0000000000000..fe7bd2441cf3f --- /dev/null +++ b/src/libs/runOnLiveMarkdownRuntime/index.native.tsx @@ -0,0 +1,8 @@ +import {getWorkletRuntime} from '@expensify/react-native-live-markdown'; +import {runOnRuntime} from 'react-native-reanimated'; + +function runOnLiveMarkdownRuntime(worklet: (...args: Args) => ReturnType) { + return runOnRuntime(getWorkletRuntime(), worklet); +} + +export default runOnLiveMarkdownRuntime; diff --git a/src/libs/runOnLiveMarkdownRuntime/index.tsx b/src/libs/runOnLiveMarkdownRuntime/index.tsx new file mode 100644 index 0000000000000..928e7a34ea3f1 --- /dev/null +++ b/src/libs/runOnLiveMarkdownRuntime/index.tsx @@ -0,0 +1,6 @@ +// Reanimated does not support runOnRuntime() on web +function runOnLiveMarkdownRuntime(worklet: (...args: Args) => ReturnType) { + return worklet; +} + +export default runOnLiveMarkdownRuntime; diff --git a/tests/perf-test/SearchRouter.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx index 089f08b14fb3a..95807a12cd1b4 100644 --- a/tests/perf-test/SearchRouter.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -69,6 +69,11 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.mock('@libs/runOnLiveMarkdownRuntime', () => { + const runOnLiveMarkdownRuntime = (worklet: (...args: Args) => ReturnValue) => worklet; + return runOnLiveMarkdownRuntime; +}); + const getMockedReports = (length = 100) => createCollection( (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, @@ -117,6 +122,7 @@ function SearchAutocompleteInputWrapper() { value={value} onSearchQueryChange={(searchTerm) => setValue(searchTerm)} isFullWidth={false} + substitutionMap={CONST.EMPTY_OBJECT} /> ); diff --git a/tests/unit/SearchAutocompleteParserTest.ts b/tests/unit/SearchAutocompleteParserTest.ts index 995d23eaeff5c..84fbf70b3aeb5 100644 --- a/tests/unit/SearchAutocompleteParserTest.ts +++ b/tests/unit/SearchAutocompleteParserTest.ts @@ -1,5 +1,5 @@ import type {SearchQueryJSON} from '@components/Search/types'; -import * as autocompleteParser from '@libs/SearchParser/autocompleteParser'; +import {parse} from '@libs/SearchParser/autocompleteParser'; import parserCommonTests from '../utils/fixtures/searchParsersCommonQueries'; const tests = [ @@ -13,7 +13,9 @@ const tests = [ length: 3, }, ranges: [ + {key: 'syntax', value: 'type', start: 0, length: 5}, {key: 'type', value: 'expense', start: 5, length: 7}, + {key: 'syntax', value: 'status', start: 13, length: 7}, {key: 'status', value: 'all', start: 20, length: 3}, ], }, @@ -23,8 +25,11 @@ const tests = [ expected: { autocomplete: null, ranges: [ + {key: 'syntax', value: 'taxRate', start: 0, length: 9}, {key: 'taxRate', value: 'rate1', start: 9, length: 5}, + {key: 'syntax', value: 'expenseType', start: 15, length: 13}, {key: 'expenseType', value: 'card', start: 28, length: 4}, + {key: 'syntax', value: 'cardID', start: 33, length: 5}, {key: 'cardID', value: 'Big Bank', start: 38, length: 10}, ], }, @@ -34,8 +39,11 @@ const tests = [ expected: { autocomplete: null, ranges: [ + {key: 'syntax', value: 'taxRate', start: 0, length: 8}, {key: 'taxRate', value: 'rate1', start: 8, length: 5}, + {key: 'syntax', value: 'expenseType', start: 14, length: 12}, {key: 'expenseType', value: 'card', start: 26, length: 4}, + {key: 'syntax', value: 'cardID', start: 31, length: 7}, {key: 'cardID', value: 'Big Bank', start: 38, length: 10}, ], }, @@ -50,8 +58,10 @@ const tests = [ value: 'meal & entertainment', }, ranges: [ + {key: 'syntax', value: 'expenseType', start: 11, length: 13}, {key: 'expenseType', length: 4, start: 24, value: 'cash'}, {key: 'expenseType', length: 4, start: 29, value: 'card'}, + {key: 'syntax', value: 'category', start: 80, length: 9}, {key: 'category', length: 6, start: 89, value: 'travel'}, {key: 'category', length: 5, start: 96, value: 'hotel'}, {key: 'category', length: 22, start: 102, value: 'meal & entertainment'}, @@ -68,8 +78,11 @@ const tests = [ value: 'a b', }, ranges: [ + {key: 'syntax', value: 'type', start: 0, length: 5}, {key: 'type', value: 'expense', start: 5, length: 7}, + {key: 'syntax', value: 'status', start: 13, length: 7}, {key: 'status', value: 'all', start: 20, length: 3}, + {key: 'syntax', value: 'category', start: 24, length: 9}, {key: 'category', value: 'a b', start: 33, length: 5}, ], }, @@ -92,7 +105,7 @@ const tests = [ query: 'tag:,,', expected: { autocomplete: null, - ranges: [], + ranges: [{key: 'syntax', value: 'tag', start: 0, length: 4}], }, }, { @@ -105,7 +118,9 @@ const tests = [ length: 3, }, ranges: [ + {key: 'syntax', value: 'in', start: 0, length: 3}, {key: 'in', value: '123456', start: 3, length: 6}, + {key: 'syntax', value: 'currency', start: 10, length: 9}, {key: 'currency', value: 'USD', start: 19, length: 3}, ], }, @@ -120,6 +135,7 @@ const tests = [ length: 4, }, ranges: [ + {key: 'syntax', value: 'tag', start: 0, length: 4}, {key: 'tag', value: 'aa', start: 4, length: 2}, {key: 'tag', value: 'bbb', start: 7, length: 3}, {key: 'tag', value: 'cccc', start: 11, length: 4}, @@ -135,7 +151,7 @@ const tests = [ start: 9, length: 0, }, - ranges: [], + ranges: [{key: 'syntax', value: 'category', start: 0, length: 9}], }, }, { @@ -147,7 +163,10 @@ const tests = [ start: 21, length: 0, }, - ranges: [{key: 'category', value: 'Advertising', start: 9, length: 11}], + ranges: [ + {key: 'syntax', value: 'category', start: 0, length: 9}, + {key: 'category', value: 'Advertising', start: 9, length: 11}, + ], }, }, { @@ -160,6 +179,7 @@ const tests = [ length: 12, }, ranges: [ + {key: 'syntax', value: 'in', start: 0, length: 3}, {key: 'in', value: 'Big Room', start: 3, length: 10}, {key: 'in', value: 'small room', start: 14, length: 12}, ], @@ -174,7 +194,10 @@ const tests = [ start: 12, length: 3, }, - ranges: [{key: 'category', value: 'Car', start: 12, length: 3}], + ranges: [ + {key: 'syntax', value: 'category', start: 0, length: 12}, + {key: 'category', value: 'Car', start: 12, length: 3}, + ], }, }, { @@ -182,7 +205,9 @@ const tests = [ expected: { autocomplete: null, ranges: [ + {key: 'syntax', value: 'type', start: 0, length: 5}, {key: 'type', value: 'expense', start: 5, length: 7}, + {key: 'syntax', value: 'status', start: 13, length: 7}, {key: 'status', value: 'all', start: 20, length: 3}, ], }, @@ -197,11 +222,16 @@ const tests = [ length: 4, }, ranges: [ + {key: 'syntax', value: 'in', start: 0, length: 3}, {key: 'in', value: 'Big Room', start: 3, length: 10}, + {key: 'syntax', value: 'from', start: 14, length: 5}, {key: 'from', value: 'Friend', start: 19, length: 6}, + {key: 'syntax', value: 'category', start: 26, length: 9}, {key: 'category', value: 'Car', start: 35, length: 3}, {key: 'category', value: 'Cell Phone', start: 39, length: 12}, + {key: 'syntax', value: 'status', start: 52, length: 7}, {key: 'status', value: 'all', start: 59, length: 3}, + {key: 'syntax', value: 'expenseType', start: 63, length: 13}, {key: 'expenseType', value: 'card', start: 76, length: 4}, {key: 'expenseType', value: 'cash', start: 81, length: 4}, ], @@ -217,11 +247,15 @@ const tests = [ length: 8, }, ranges: [ + {key: 'syntax', value: 'currency', start: 0, length: 9}, {key: 'currency', value: 'PLN', start: 9, length: 3}, {key: 'currency', value: 'USD', start: 13, length: 3}, + {key: 'syntax', value: 'taxRate', start: 25, length: 9}, {key: 'taxRate', value: 'tax', start: 34, length: 3}, + {key: 'syntax', value: 'tag', start: 66, length: 4}, {key: 'tag', value: 'General Overhead', start: 70, length: 18}, {key: 'tag', value: 'IT', start: 89, length: 2}, + {key: 'syntax', value: 'expenseType', start: 92, length: 13}, {key: 'expenseType', value: 'card', start: 105, length: 4}, {key: 'expenseType', value: 'distance', start: 110, length: 8}, ], @@ -231,7 +265,7 @@ const tests = [ describe('autocomplete parser', () => { test.each(tests)(`parsing: $query`, ({query, expected}) => { - const result = autocompleteParser.parse(query) as SearchQueryJSON; + const result = parse(query) as SearchQueryJSON; expect(result).toEqual(expected); });