From fa5c8bcfffd6f8bce64c662b6e99aba24c79fe51 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Thu, 19 Dec 2024 15:53:10 +0100 Subject: [PATCH 01/19] add script for parser workletization --- package.json | 2 +- scripts/parser-workletization.sh | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100755 scripts/parser-workletization.sh diff --git a/package.json b/package.json index 02ef81489e014..094604130d6e4 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", "react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt", "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy", - "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy", + "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy && ./scripts/parser-workletization.sh src/libs/SearchParser/autocompleteParser.js", "web:prod": "http-server ./dist --cors" }, "dependencies": { diff --git a/scripts/parser-workletization.sh b/scripts/parser-workletization.sh new file mode 100755 index 0000000000000..7611531113676 --- /dev/null +++ b/scripts/parser-workletization.sh @@ -0,0 +1,22 @@ +#!/bin/bash +### +# This script modifies the autocompleteParser.js file to be compatible with worklets. +# autocompleteParser.js is generated by PeggyJS and uses syntax not supported by worklets. +# This script runs each time the parser is generated by the `generate-autocomplete-parser` command. +### + +filePath=$1 + +if [ ! -f $filePath ]; then + echo "$filePath does not exist." + exit 1 +fi +awk 'BEGIN { print "\47worklet\47\n\nclass peg\$SyntaxError{}" } 1' $filePath | sed 's/function peg\$SyntaxError/function temporary/g' | sed 's/peg$subclass(peg$SyntaxError, Error);//g' > tmp.txt +if [ $? -eq 0 ]; then + mv tmp.txt $filePath + echo "Successfully updated $filePath" +else + echo "An error occurred while modifying the file." + rm -f tmp.txt + exit 1 +fi \ No newline at end of file From cc24f2563464547a2e780f444ae9b8f0cb4f09f6 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Thu, 19 Dec 2024 15:55:37 +0100 Subject: [PATCH 02/19] update parser --- src/libs/SearchParser/autocompleteParser.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 0b456b5823b1c..02a1d4e091606 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -1,3 +1,6 @@ +'worklet' + +class peg$SyntaxError{} // @generated by Peggy 4.0.3. // // https://peggyjs.org/ @@ -9,7 +12,7 @@ function peg$subclass(child, parent) { child.prototype = new C(); } -function peg$SyntaxError(message, expected, found, location) { +function temporary(message, expected, found, location) { var self = Error.call(this, message); // istanbul ignore next Check is a necessary evil to support older environments if (Object.setPrototypeOf) { @@ -22,7 +25,7 @@ function peg$SyntaxError(message, expected, found, location) { return self; } -peg$subclass(peg$SyntaxError, Error); + function peg$padEnd(str, targetLength, padString) { padString = padString || " "; From 8a7ea58075b45f09212ff873158ddd045d7c54ce Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 20 Dec 2024 11:56:53 +0100 Subject: [PATCH 03/19] Improve parsers - handle NBSP --- src/libs/SearchParser/autocompleteParser.js | 14 +++++++------- src/libs/SearchParser/baseRules.peggy | 6 +++--- src/libs/SearchParser/searchParser.js | 20 ++++++++++---------- src/libs/SearchParser/searchParser.peggy | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 02a1d4e091606..f410fe58d2b4d 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -218,10 +218,10 @@ function peg$parse(input, options) { var peg$c37 = "\""; var peg$r0 = /^[:=]/; - var peg$r1 = /^[^ ,"\t\n\r]/; + var peg$r1 = /^[^ ,"\t\n\r\xA0]/; var peg$r2 = /^[^"\r\n]/; - var peg$r3 = /^[^ ,\t\n\r]/; - var peg$r4 = /^[ \t\r\n]/; + var peg$r3 = /^[^ ,\t\n\r\xA0]/; + var peg$r4 = /^[ \t\r\n\xA0]/; var peg$e0 = peg$literalExpectation(",", false); var peg$e1 = peg$otherExpectation("key"); @@ -264,13 +264,13 @@ function peg$parse(input, options) { var peg$e38 = peg$literalExpectation("<=", false); var peg$e39 = peg$literalExpectation("<", false); var peg$e40 = peg$otherExpectation("quote"); - var peg$e41 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false); + var peg$e41 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r", "\xA0"], true, false); var peg$e42 = peg$literalExpectation("\"", false); var peg$e43 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e44 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false); + var peg$e44 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); var peg$e45 = peg$otherExpectation("word"); var peg$e46 = peg$otherExpectation("whitespace"); - var peg$e47 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e47 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); var peg$f0 = function(ranges) { return { autocomplete, ranges }; }; var peg$f1 = function(filters) { return filters.filter(Boolean).flat(); }; @@ -351,7 +351,7 @@ function peg$parse(input, options) { var peg$f33 = function() { return "gt"; }; var peg$f34 = function() { return "lte"; }; var peg$f35 = function() { return "lt"; }; - var peg$f36 = function(start, inner, end) { + var peg$f36 = function(start, inner, end) { //handle no-breaking-space return [...start, '"', ...inner, '"', ...end].join(""); }; var peg$f37 = function(chars) { return chars.join("").trim(); }; diff --git a/src/libs/SearchParser/baseRules.peggy b/src/libs/SearchParser/baseRules.peggy index cc1305adc8b36..8ee6df0e73bdc 100644 --- a/src/libs/SearchParser/baseRules.peggy +++ b/src/libs/SearchParser/baseRules.peggy @@ -56,12 +56,12 @@ operator "operator" / "<" { return "lt"; } quotedString "quote" - = start:[^ ,"\t\n\r]* "\"" inner:[^"\r\n]* "\"" end:[^ ,\t\n\r]* { + = start:[^ ,"\t\n\r\xA0]* "\"" inner:[^"\r\n]* "\"" end:[^ ,\t\n\r\xA0]* { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); } -alphanumeric "word" = chars:[^ ,\t\n\r]+ { return chars.join("").trim(); } +alphanumeric "word" = chars:[^ ,\t\n\r\xA0]+ { return chars.join("").trim(); } //handle no-breaking space logicalAnd = _ { return "and"; } -_ "whitespace" = [ \t\r\n]* +_ "whitespace" = [ \t\r\n\xA0]* //handle no-breaking space diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 47b534d32cad0..19fcb1a3049e1 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -219,14 +219,14 @@ function peg$parse(input, options) { var peg$c36 = "<"; var peg$c37 = "\""; - var peg$r0 = /^[^ \t\r\n]/; + var peg$r0 = /^[^ \t\r\n\xA0]/; var peg$r1 = /^[:=]/; - var peg$r2 = /^[^ ,"\t\n\r]/; + var peg$r2 = /^[^ ,"\t\n\r\xA0]/; var peg$r3 = /^[^"\r\n]/; - var peg$r4 = /^[^ ,\t\n\r]/; - var peg$r5 = /^[ \t\r\n]/; + var peg$r4 = /^[^ ,\t\n\r\xA0]/; + var peg$r5 = /^[ \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); @@ -269,13 +269,13 @@ function peg$parse(input, options) { var peg$e40 = peg$literalExpectation("<=", false); var peg$e41 = peg$literalExpectation("<", false); var peg$e42 = peg$otherExpectation("quote"); - var peg$e43 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false); + var peg$e43 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r", "\xA0"], true, false); var peg$e44 = peg$literalExpectation("\"", false); var peg$e45 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e46 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false); + var peg$e46 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); var peg$e47 = peg$otherExpectation("word"); var peg$e48 = peg$otherExpectation("whitespace"); - var peg$e49 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + var peg$e49 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { @@ -310,7 +310,7 @@ function peg$parse(input, options) { var peg$f2 = function(key, op, value) { updateDefaultValues(key, value); }; - var peg$f3 = function(value) { + var peg$f3 = function(value) { //handle no-breaking-space if (Array.isArray(value)) { return buildFilter("eq", "keyword", value.join("")); } @@ -362,7 +362,7 @@ function peg$parse(input, options) { var peg$f34 = function() { return "gt"; }; var peg$f35 = function() { return "lte"; }; var peg$f36 = function() { return "lt"; }; - var peg$f37 = function(start, inner, end) { + var peg$f37 = function(start, inner, end) { //handle no-breaking-space return [...start, '"', ...inner, '"', ...end].join(""); }; var peg$f38 = function(chars) { return chars.join("").trim(); }; diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 732bb430d5269..768daa51a1119 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -84,7 +84,7 @@ defaultFilter } freeTextFilter - = _ value:(quotedString / [^ \t\r\n]+) _ { + = _ value:(quotedString / [^ \t\r\n\xA0]+) _ { //handle no-breaking space if (Array.isArray(value)) { return buildFilter("eq", "keyword", value.join("")); } From 5d38a0548bca3b38c2f0965ee90ac9a2cf78bc71 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 20 Dec 2024 11:58:37 +0100 Subject: [PATCH 04/19] improve sanitize string and and workletized parser function --- src/libs/SearchAutocompleteUtils.ts | 13 ++++++++++++- src/libs/SearchQueryUtils.ts | 3 +-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index fe6988033dd9d..341c40f8b1def 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -1,3 +1,4 @@ +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 ONYXKEYS from '@src/ONYXKEYS'; @@ -14,7 +15,7 @@ function parseForAutocomplete(text: string) { const parsedAutocomplete = autocompleteParser.parse(text) as SearchAutocompleteResult; return parsedAutocomplete; } catch (e) { - console.error(`Error when parsing autocopmlete query"`, e); + console.error(`Error when parsing autocomplete query"`, e); } } @@ -131,6 +132,15 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { return newQuery; } +function workletizedParser(input: string) { + 'worklet'; + + const parsedAutocomplete = autocompleteParser.parse(input) as SearchAutocompleteResult; + const ranges = parsedAutocomplete.ranges; + // TODO: change type depending on range + return ranges.map((range) => ({...range, type: 'mention-user'})) as MarkdownRange[]; +} + export { parseForAutocomplete, getAutocompleteTags, @@ -140,4 +150,5 @@ export { getAutocompleteTaxList, getQueryWithoutAutocompletedPart, getAutocompleteQueryWithComma, + workletizedParser, }; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 73c83cb33b837..1c92b0f848b39 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -70,8 +70,7 @@ const UserFriendlyKeyMap: Record Date: Fri, 20 Dec 2024 12:01:44 +0100 Subject: [PATCH 05/19] Migrate SearchRouterInput to RNMarkdownTextInput --- .../Search/SearchPageHeaderInput.tsx | 16 ++- .../Search/SearchRouter/SearchRouter.tsx | 20 +++- .../index.native.tsx} | 53 ++------- .../SearchRouter/SearchRouterInput/index.tsx | 101 ++++++++++++++++++ .../SearchRouter/SearchRouterInput/types.ts | 49 +++++++++ .../Search/SearchRouter/SearchRouterList.tsx | 2 +- .../TextInput/BaseTextInput/types.ts | 5 +- 7 files changed, 192 insertions(+), 54 deletions(-) rename src/components/Search/SearchRouter/{SearchRouterInput.tsx => SearchRouterInput/index.native.tsx} (71%) create mode 100644 src/components/Search/SearchRouter/SearchRouterInput/index.tsx create mode 100644 src/components/Search/SearchRouter/SearchRouterInput/types.ts diff --git a/src/components/Search/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeaderInput.tsx index d9884b1c1efef..d8412447886cc 100644 --- a/src/components/Search/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeaderInput.tsx @@ -81,6 +81,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps const [textInputValue, setTextInputValue] = useState(queryText); // The input text that was last used for autocomplete; needed for the SearchRouterList when browsing list via arrow keys const [autocompleteQueryValue, setAutocompleteQueryValue] = useState(queryText); + const [selection, setSelection] = useState({start: textInputValue.length, end: textInputValue.length}); const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState({}); const [isAutocompleteListVisible, setIsAutocompleteListVisible] = useState(false); @@ -158,7 +159,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) { const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(textInputValue); - onSearchQueryChange(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)} `); + const newSearchQuery = `${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)}\u00A0`; + onSearchQueryChange(newSearchQuery); + setSelection({start: newSearchQuery.length, end: newSearchQuery.length}); if (item.mapKey && item.autocompleteID) { const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID}; @@ -189,6 +192,14 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps [autocompleteSubstitutions], ); + const setTextAndUpdateSelection = useCallback( + (text: string) => { + setTextInputValue(text); + setSelection({start: text.length, end: text.length}); + }, + [setSelection, setTextInputValue], + ); + if (isCannedQuery) { const headerIcon = getHeaderContent(type).icon; @@ -267,13 +278,14 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps rightComponent={children} routerListRef={listRef} ref={textInputRef} + selection={selection} /> diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 4b800b6377122..4e88caa656d56 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -87,6 +87,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) const [textInputValue, , setTextInputValue] = useDebouncedState('', 500); // The input text that was last used for autocomplete; needed for the SearchRouterList when browsing list via arrow keys const [autocompleteQueryValue, setAutocompleteQueryValue] = useState(textInputValue); + const [selection, setSelection] = useState({start: textInputValue.length, end: textInputValue.length}); const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState({}); const textInputRef = useRef(null); @@ -202,6 +203,14 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) [autocompleteSubstitutions, onRouterClose, setTextInputValue, activeWorkspaceID], ); + const setTextAndUpdateSelection = useCallback( + (text: string) => { + setTextInputValue(text); + setSelection({start: text.length, end: text.length}); + }, + [setSelection, setTextInputValue], + ); + const onListItemPress = useCallback( (item: OptionData | SearchQueryItem) => { if (isSearchQueryItem(item)) { @@ -211,7 +220,9 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION) { const searchQuery = getContextualSearchQuery(item); - onSearchQueryChange(`${searchQuery} `, true); + const newSearchQuery = `${searchQuery}\u00A0`; + onSearchQueryChange(newSearchQuery, true); + setSelection({start: newSearchQuery.length, end: newSearchQuery.length}); const autocompleteKey = getContextualSearchAutocompleteKey(item); if (autocompleteKey && item.autocompleteID) { @@ -221,7 +232,9 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) } } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) { const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(textInputValue); - onSearchQueryChange(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)} `); + const newSearchQuery = `${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)}\u00A0`; + onSearchQueryChange(newSearchQuery); + setSelection({start: newSearchQuery.length, end: newSearchQuery.length}); if (item.mapKey && item.autocompleteID) { const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID}; @@ -292,6 +305,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]} wrapperFocusedStyle={[styles.borderColorFocus]} isSearchingForReports={isSearchingForReports} + selection={selection} ref={textInputRef} /> diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx similarity index 71% rename from src/components/Search/SearchRouter/SearchRouterInput.tsx rename to src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx index e6a7af37b1bbe..b0ae39be8dc0e 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx @@ -1,61 +1,17 @@ -import type {ForwardedRef, ReactNode, RefObject} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useState} from 'react'; -import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {View} from 'react-native'; 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 useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import {workletizedParser} from '@libs/SearchAutocompleteUtils'; import shouldDelayFocus from '@libs/shouldDelayFocus'; import variables from '@styles/variables'; import CONST from '@src/CONST'; - -type SearchRouterInputProps = { - /** Value of TextInput */ - value: string; - - /** Callback to update search in SearchRouter */ - onSearchQueryChange: (searchTerm: string) => void; - - /** Callback invoked when the user submits the input */ - onSubmit?: () => void; - - /** SearchRouterList ref for managing TextInput and SearchRouterList focus */ - routerListRef?: RefObject; - - /** Whether the input is full width */ - isFullWidth: boolean; - - /** Whether the input is disabled */ - disabled?: boolean; - - /** Whether the offline message should be shown */ - shouldShowOfflineMessage?: boolean; - - /** Callback to call when the input gets focus */ - onFocus?: () => void; - - /** Callback to call when the input gets blur */ - onBlur?: () => void; - - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; - - /** Any additional styles to apply when input is focused */ - wrapperFocusedStyle?: StyleProp; - - /** Any additional styles to apply to text input along with FormHelperMessage */ - outerWrapperStyle?: StyleProp; - - /** Component to be displayed on the right */ - rightComponent?: ReactNode; - - /** Whether the search reports API call is running */ - isSearchingForReports?: boolean; -} & Pick; +import type SearchRouterInputProps from './types'; function SearchRouterInput( { @@ -122,6 +78,9 @@ function SearchRouterInput( }} isLoading={!!isSearchingForReports} ref={ref} + isMarkdownEnabled + multiline={false} + parser={workletizedParser} /> {!!rightComponent && {rightComponent}} diff --git a/src/components/Search/SearchRouter/SearchRouterInput/index.tsx b/src/components/Search/SearchRouter/SearchRouterInput/index.tsx new file mode 100644 index 0000000000000..18498869ab043 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterInput/index.tsx @@ -0,0 +1,101 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useState} from 'react'; +import {View} from 'react-native'; +import FormHelpMessage from '@components/FormHelpMessage'; +import TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {workletizedParser} from '@libs/SearchAutocompleteUtils'; +import shouldDelayFocus from '@libs/shouldDelayFocus'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type SearchRouterInputProps from './types'; + +function SearchRouterInput( + { + value, + onSearchQueryChange, + onSubmit = () => {}, + routerListRef, + isFullWidth, + disabled = false, + shouldShowOfflineMessage = false, + autoFocus = true, + onFocus, + onBlur, + caretHidden = false, + wrapperStyle, + wrapperFocusedStyle, + outerWrapperStyle, + rightComponent, + isSearchingForReports, + selection, + }: SearchRouterInputProps, + ref: ForwardedRef, +) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [isFocused, setIsFocused] = useState(false); + const {isOffline} = useNetwork(); + const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + + const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; + + return ( + + + + { + setIsFocused(true); + routerListRef?.current?.updateExternalTextInputFocus(true); + onFocus?.(); + }} + onBlur={() => { + setIsFocused(false); + routerListRef?.current?.updateExternalTextInputFocus(false); + onBlur?.(); + }} + isLoading={!!isSearchingForReports} + ref={ref} + isMarkdownEnabled + multiline={false} + parser={workletizedParser} + selection={selection} + /> + + {!!rightComponent && {rightComponent}} + + + + ); +} + +SearchRouterInput.displayName = 'SearchRouterInput'; + +export default forwardRef(SearchRouterInput); diff --git a/src/components/Search/SearchRouter/SearchRouterInput/types.ts b/src/components/Search/SearchRouter/SearchRouterInput/types.ts new file mode 100644 index 0000000000000..da126c8085eaf --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterInput/types.ts @@ -0,0 +1,49 @@ +import type {ReactNode, RefObject} from 'react'; +import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; +import type {SelectionListHandle} from '@components/SelectionList/types'; + +type SearchRouterInputProps = { + /** Value of TextInput */ + value: string; + + /** Callback to update search in SearchRouter */ + onSearchQueryChange: (searchTerm: string) => void; + + /** Callback invoked when the user submits the input */ + onSubmit?: () => void; + + /** SearchRouterList ref for managing TextInput and SearchRouterList focus */ + routerListRef?: RefObject; + + /** Whether the input is full width */ + isFullWidth: boolean; + + /** Whether the input is disabled */ + disabled?: boolean; + + /** Whether the offline message should be shown */ + shouldShowOfflineMessage?: boolean; + + /** Callback to call when the input gets focus */ + onFocus?: () => void; + + /** Callback to call when the input gets blur */ + onBlur?: () => void; + + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; + + /** Any additional styles to apply when input is focused */ + wrapperFocusedStyle?: StyleProp; + + /** Any additional styles to apply to text input along with FormHelperMessage */ + outerWrapperStyle?: StyleProp; + + /** Component to be displayed on the right */ + rightComponent?: ReactNode; + + /** Whether the search reports API call is running */ + isSearchingForReports?: boolean; +} & Pick; + +export default SearchRouterInputProps; diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index a53e49374d816..2c29db12cc135 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -443,7 +443,7 @@ function SearchRouterList( } const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(autocompleteQueryValue); - setTextQuery(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(focusedItem.searchQuery)} `); + setTextQuery(`${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(focusedItem.searchQuery)}\u00A0`); updateAutocompleteSubstitutions(focusedItem); }, [autocompleteQueryValue, setTextQuery, updateAutocompleteSubstitutions], diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index c9844e33d594e..027613e301f84 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,4 +1,4 @@ -import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; +import type {MarkdownRange, MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -119,6 +119,9 @@ type CustomBaseTextInputProps = { /** List of markdowns that won't be styled as a markdown */ excludedMarkdownStyles?: Array; + /** Custom parser function for RNMarkdownTextInput */ + parser?: (input: string) => MarkdownRange[]; + /** Whether the clear button should be displayed */ shouldShowClearButton?: boolean; From b5d82de323f8d71b94953c2b46df128fe14f0559 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 20 Dec 2024 12:18:21 +0100 Subject: [PATCH 06/19] fix script lint --- scripts/parser-workletization.sh | 9 ++++----- src/libs/SearchParser/autocompleteParser.js | 2 +- src/libs/SearchParser/searchParser.js | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/scripts/parser-workletization.sh b/scripts/parser-workletization.sh index 7611531113676..2cb6e715d8abb 100755 --- a/scripts/parser-workletization.sh +++ b/scripts/parser-workletization.sh @@ -7,16 +7,15 @@ filePath=$1 -if [ ! -f $filePath ]; then +if [ ! -f "$filePath" ]; then echo "$filePath does not exist." exit 1 fi -awk 'BEGIN { print "\47worklet\47\n\nclass peg\$SyntaxError{}" } 1' $filePath | sed 's/function peg\$SyntaxError/function temporary/g' | sed 's/peg$subclass(peg$SyntaxError, Error);//g' > tmp.txt -if [ $? -eq 0 ]; then - mv tmp.txt $filePath +if awk 'BEGIN { print "\47worklet\47\n\nclass peg\$SyntaxError{}" } 1' "$filePath" | sed 's/function peg\$SyntaxError/function temporary/g' | sed 's/peg$subclass(peg$SyntaxError, Error);//g' > tmp.txt; then + mv tmp.txt "$filePath" echo "Successfully updated $filePath" else echo "An error occurred while modifying the file." rm -f tmp.txt exit 1 -fi \ No newline at end of file +fi diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index f410fe58d2b4d..557a5590a637f 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -351,7 +351,7 @@ function peg$parse(input, options) { var peg$f33 = function() { return "gt"; }; var peg$f34 = function() { return "lte"; }; var peg$f35 = function() { return "lt"; }; - var peg$f36 = function(start, inner, end) { //handle no-breaking-space + var peg$f36 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; var peg$f37 = function(chars) { return chars.join("").trim(); }; diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 19fcb1a3049e1..10b913be74ec8 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -310,7 +310,7 @@ function peg$parse(input, options) { var peg$f2 = function(key, op, value) { updateDefaultValues(key, value); }; - var peg$f3 = function(value) { //handle no-breaking-space + var peg$f3 = function(value) { //handle no-breaking space if (Array.isArray(value)) { return buildFilter("eq", "keyword", value.join("")); } @@ -362,7 +362,7 @@ function peg$parse(input, options) { var peg$f34 = function() { return "gt"; }; var peg$f35 = function() { return "lte"; }; var peg$f36 = function() { return "lt"; }; - var peg$f37 = function(start, inner, end) { //handle no-breaking-space + var peg$f37 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; var peg$f38 = function(chars) { return chars.join("").trim(); }; From 2a65f020ecf63494259d7dde8c8790d92f76f940 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 20 Dec 2024 15:56:18 +0100 Subject: [PATCH 07/19] fix shell lint and add styles --- scripts/parser-workletization.sh | 1 + src/components/Search/SearchRouter/SearchRouterInput/index.tsx | 2 +- src/libs/SearchQueryUtils.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/parser-workletization.sh b/scripts/parser-workletization.sh index 2cb6e715d8abb..13d72ea0ab1f1 100755 --- a/scripts/parser-workletization.sh +++ b/scripts/parser-workletization.sh @@ -11,6 +11,7 @@ if [ ! -f "$filePath" ]; then echo "$filePath does not exist." exit 1 fi +# shellcheck disable=SC2016 if awk 'BEGIN { print "\47worklet\47\n\nclass peg\$SyntaxError{}" } 1' "$filePath" | sed 's/function peg\$SyntaxError/function temporary/g' | sed 's/peg$subclass(peg$SyntaxError, Error);//g' > tmp.txt; then mv tmp.txt "$filePath" echo "Successfully updated $filePath" diff --git a/src/components/Search/SearchRouter/SearchRouterInput/index.tsx b/src/components/Search/SearchRouter/SearchRouterInput/index.tsx index 18498869ab043..d18a8377240ad 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput/index.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput/index.tsx @@ -66,7 +66,7 @@ function SearchRouterInput( onSubmitEditing={onSubmit} shouldUseDisabledStyles={false} textInputContainerStyles={[styles.borderNone, styles.pb0]} - inputStyle={[inputWidth, styles.p3]} + inputStyle={[inputWidth, styles.p3, styles.dFlex, styles.alignItemsCenter]} onFocus={() => { setIsFocused(true); routerListRef?.current?.updateExternalTextInputFocus(true); diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 1c92b0f848b39..4ef0f60cc1c0d 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -67,7 +67,7 @@ const UserFriendlyKeyMap: Record Date: Wed, 8 Jan 2025 10:39:46 +0100 Subject: [PATCH 08/19] fix lint on chnaged files --- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 4e88caa656d56..eda7b10fd565a 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -123,7 +123,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) } if (reportForContextualSearch.isPolicyExpenseChat) { roomType = CONST.SEARCH.DATA_TYPES.EXPENSE; - autocompleteID = reportForContextualSearch.policyID ?? ''; + autocompleteID = ''; } additionalSections.push({ From 95c2690234fe8b8e41296c0ba0f1d778960942c9 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Wed, 8 Jan 2025 11:22:37 +0100 Subject: [PATCH 09/19] changes current user mention color --- .../SearchRouterInput/index.native.tsx | 10 +++++++-- .../SearchRouter/SearchRouterInput/index.tsx | 6 +++-- src/libs/SearchAutocompleteUtils.ts | 22 +++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx b/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx index bc486645c7929..876b1918b2587 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx @@ -4,10 +4,11 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import {workletizedParser} from '@libs/SearchAutocompleteUtils'; +import {parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; import shouldDelayFocus from '@libs/shouldDelayFocus'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -38,6 +39,7 @@ function SearchRouterInput( const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); const {isOffline} = useNetwork(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; @@ -81,7 +83,11 @@ function SearchRouterInput( ref={ref} isMarkdownEnabled multiline={false} - parser={workletizedParser} + parser={(input: string) => { + 'worklet'; + + return parseForLiveMarkdown(input, currentUserPersonalDetails.login ?? '', currentUserPersonalDetails.displayName ?? ''); + }} /> {!!rightComponent && {rightComponent}} diff --git a/src/components/Search/SearchRouter/SearchRouterInput/index.tsx b/src/components/Search/SearchRouter/SearchRouterInput/index.tsx index d18a8377240ad..d69b1bb168626 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput/index.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput/index.tsx @@ -4,10 +4,11 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import {workletizedParser} from '@libs/SearchAutocompleteUtils'; +import {parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; import shouldDelayFocus from '@libs/shouldDelayFocus'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -39,6 +40,7 @@ function SearchRouterInput( const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); const {isOffline} = useNetwork(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; @@ -81,7 +83,7 @@ function SearchRouterInput( ref={ref} isMarkdownEnabled multiline={false} - parser={workletizedParser} + parser={(input: string) => parseForLiveMarkdown(input, currentUserPersonalDetails.login ?? '', currentUserPersonalDetails.displayName ?? '')} selection={selection} /> diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 341c40f8b1def..162c5e27fd8e2 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -1,6 +1,7 @@ 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 CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags} from '@src/types/onyx'; import {getTagNamesFromTagsLists} from './PolicyUtils'; @@ -132,13 +133,26 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { return newQuery; } -function workletizedParser(input: string) { +/** + * 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, userLogin: string, userDisplayName: string) { 'worklet'; const parsedAutocomplete = autocompleteParser.parse(input) as SearchAutocompleteResult; const ranges = parsedAutocomplete.ranges; - // TODO: change type depending on range - return ranges.map((range) => ({...range, type: 'mention-user'})) as MarkdownRange[]; + + return ranges.map((range) => { + let type = 'mention-user'; + + if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (range.value === userLogin || range.value === userDisplayName)) { + type = 'mention-here'; + } + + return {...range, type}; + }) as MarkdownRange[]; } export { @@ -150,5 +164,5 @@ export { getAutocompleteTaxList, getQueryWithoutAutocompletedPart, getAutocompleteQueryWithComma, - workletizedParser, + parseForLiveMarkdown, }; From 9b80cd0e0cb239fd927b6a940ff0f50b5e76ccab Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Mon, 13 Jan 2025 12:19:11 +0100 Subject: [PATCH 10/19] logic separation and names refactor --- scripts/parser-workletization.sh | 2 +- ...native.tsx => SearchAutocompleteInput.tsx} | 74 ++++++++++-- ...terList.tsx => SearchAutocompleteList.tsx} | 13 +-- .../index.native.tsx | 23 ++++ .../SearchInputSelectionWrapper/index.tsx | 23 ++++ .../Search/SearchPageHeaderInput.tsx | 12 +- .../Search/SearchRouter/SearchRouter.tsx | 12 +- .../SearchRouter/SearchRouterInput/index.tsx | 106 ------------------ .../SearchRouter/SearchRouterInput/types.ts | 49 -------- src/components/SelectionList/types.ts | 2 +- tests/perf-test/SearchRouter.perf-test.tsx | 10 +- 11 files changed, 134 insertions(+), 192 deletions(-) rename src/components/Search/{SearchRouter/SearchRouterInput/index.native.tsx => SearchAutocompleteInput.tsx} (63%) rename src/components/Search/{SearchRouter/SearchRouterList.tsx => SearchAutocompleteList.tsx} (98%) create mode 100644 src/components/Search/SearchInputSelectionWrapper/index.native.tsx create mode 100644 src/components/Search/SearchInputSelectionWrapper/index.tsx delete mode 100644 src/components/Search/SearchRouter/SearchRouterInput/index.tsx delete mode 100644 src/components/Search/SearchRouter/SearchRouterInput/types.ts diff --git a/scripts/parser-workletization.sh b/scripts/parser-workletization.sh index 13d72ea0ab1f1..ab048e407de33 100755 --- a/scripts/parser-workletization.sh +++ b/scripts/parser-workletization.sh @@ -1,7 +1,7 @@ #!/bin/bash ### # This script modifies the autocompleteParser.js file to be compatible with worklets. -# autocompleteParser.js is generated by PeggyJS and uses syntax not supported by worklets. +# autocompleteParser.js is generated by PeggyJS and uses some parts of syntax not supported by worklets. # This script runs each time the parser is generated by the `generate-autocomplete-parser` command. ### diff --git a/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx b/src/components/Search/SearchAutocompleteInput.tsx similarity index 63% rename from src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx rename to src/components/Search/SearchAutocompleteInput.tsx index 4800fcd68132b..5ab33aed5088b 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput/index.native.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -1,7 +1,9 @@ -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, ReactNode, RefObject} from 'react'; import React, {forwardRef, useState} from 'react'; import {View} from 'react-native'; +import type {StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; 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 useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -13,14 +15,60 @@ import handleKeyPress from '@libs/SearchInputOnKeyPress'; import shouldDelayFocus from '@libs/shouldDelayFocus'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type SearchRouterInputProps from './types'; -function SearchRouterInput( +type SearchAutocompleteInputProps = { + /** Value of TextInput */ + value: string; + + /** Callback to update search in SearchRouter */ + onSearchQueryChange: (searchTerm: string) => void; + + /** Callback invoked when the user submits the input */ + onSubmit?: () => void; + + /** SearchAutocompleteList ref for managing TextInput and SearchAutocompleteList focus */ + autocompleteListRef?: RefObject; + + /** Whether the input is full width */ + isFullWidth: boolean; + + /** Whether the input is disabled */ + disabled?: boolean; + + /** Whether the offline message should be shown */ + shouldShowOfflineMessage?: boolean; + + /** Callback to call when the input gets focus */ + onFocus?: () => void; + + /** Callback to call when the input gets blur */ + onBlur?: () => void; + + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; + + /** Any additional styles to apply when input is focused */ + wrapperFocusedStyle?: StyleProp; + + /** Any additional styles to apply to text input along with FormHelperMessage */ + outerWrapperStyle?: StyleProp; + + /** Component to be displayed on the right */ + rightComponent?: ReactNode; + + /** Whether the search reports API call is running */ + isSearchingForReports?: boolean; + + /** input style */ + inputStyle?: StyleProp; +} & Pick; + +function SearchAutocompleteInput( { value, onSearchQueryChange, onSubmit = () => {}, - routerListRef, + autocompleteListRef, isFullWidth, disabled = false, shouldShowOfflineMessage = false, @@ -33,7 +81,9 @@ function SearchRouterInput( outerWrapperStyle, rightComponent, isSearchingForReports, - }: SearchRouterInputProps, + selection, + inputStyle, + }: SearchAutocompleteInputProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -50,7 +100,7 @@ function SearchRouterInput( { setIsFocused(true); - routerListRef?.current?.updateExternalTextInputFocus(true); + autocompleteListRef?.current?.updateExternalTextInputFocus(true); onFocus?.(); }} onBlur={() => { setIsFocused(false); - routerListRef?.current?.updateExternalTextInputFocus(false); + autocompleteListRef?.current?.updateExternalTextInputFocus(false); onBlur?.(); }} isLoading={!!isSearchingForReports} @@ -90,6 +140,7 @@ function SearchRouterInput( return parseForLiveMarkdown(input, currentUserPersonalDetails.login ?? '', currentUserPersonalDetails.displayName ?? ''); }} + selection={selection} /> {!!rightComponent && {rightComponent}} @@ -103,6 +154,7 @@ function SearchRouterInput( ); } -SearchRouterInput.displayName = 'SearchRouterInput'; +SearchAutocompleteInput.displayName = 'SearchAutocompleteInput'; -export default forwardRef(SearchRouterInput); +export type {SearchAutocompleteInputProps}; +export default forwardRef(SearchAutocompleteInput); diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchAutocompleteList.tsx similarity index 98% rename from src/components/Search/SearchRouter/SearchRouterList.tsx rename to src/components/Search/SearchAutocompleteList.tsx index 263ebf7c580fa..90a946137c315 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -5,7 +5,6 @@ import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; -import type {SearchFilterKey, UserFriendlyKey} from '@components/Search/types'; import SelectionList from '@components/SelectionList'; import SearchQueryListItem, {isSearchQueryItem} from '@components/SelectionList/Search/SearchQueryListItem'; import type {SearchQueryItem, SearchQueryListItemProps} from '@components/SelectionList/Search/SearchQueryListItem'; @@ -38,7 +37,8 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type PersonalDetails from '@src/types/onyx/PersonalDetails'; -import {getSubstitutionMapKey} from './getQueryWithSubstitutions'; +import {getSubstitutionMapKey} from './SearchRouter/getQueryWithSubstitutions'; +import type {SearchFilterKey, UserFriendlyKey} from './types'; type AutocompleteItemData = { filterKey: UserFriendlyKey; @@ -47,7 +47,7 @@ type AutocompleteItemData = { mapKey?: SearchFilterKey; }; -type SearchRouterListProps = { +type SearchAutocompleteListProps = { /** Value of TextInput */ autocompleteQueryValue: string; @@ -115,9 +115,8 @@ function SearchRouterItem(props: UserListItemProps | SearchQueryList ); } -// Todo rename to SearchAutocompleteList once it's used in both Router and SearchPage -function SearchRouterList( - {autocompleteQueryValue, searchQueryItem, additionalSections, onListItemPress, setTextQuery, updateAutocompleteSubstitutions}: SearchRouterListProps, +function SearchAutocompleteList( + {autocompleteQueryValue, searchQueryItem, additionalSections, onListItemPress, setTextQuery, updateAutocompleteSubstitutions}: SearchAutocompleteListProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -483,5 +482,5 @@ function SearchRouterList( ); } -export default forwardRef(SearchRouterList); +export default forwardRef(SearchAutocompleteList); export {SearchRouterItem}; diff --git a/src/components/Search/SearchInputSelectionWrapper/index.native.tsx b/src/components/Search/SearchInputSelectionWrapper/index.native.tsx new file mode 100644 index 0000000000000..8e6fcca4dcf81 --- /dev/null +++ b/src/components/Search/SearchInputSelectionWrapper/index.native.tsx @@ -0,0 +1,23 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput'; +import type {SearchAutocompleteInputProps} from '@components/Search/SearchAutocompleteInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function SearchInputSelectionWrapper(props: SearchAutocompleteInputProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + return ( + + ); +} + +SearchInputSelectionWrapper.displayName = 'SearchInputSelectionWrapper'; + +export default forwardRef(SearchInputSelectionWrapper); diff --git a/src/components/Search/SearchInputSelectionWrapper/index.tsx b/src/components/Search/SearchInputSelectionWrapper/index.tsx new file mode 100644 index 0000000000000..a806cea9afd45 --- /dev/null +++ b/src/components/Search/SearchInputSelectionWrapper/index.tsx @@ -0,0 +1,23 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput'; +import type {SearchAutocompleteInputProps} from '@components/Search/SearchAutocompleteInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function SearchInputSelectionWrapper({selection, ...props}: SearchAutocompleteInputProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + return ( + + ); +} + +SearchInputSelectionWrapper.displayName = 'SearchInputSelectionWrapper'; + +export default forwardRef(SearchInputSelectionWrapper); diff --git a/src/components/Search/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeaderInput.tsx index d8412447886cc..dec6275568af2 100644 --- a/src/components/Search/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeaderInput.tsx @@ -28,14 +28,14 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; import type IconAsset from '@src/types/utils/IconAsset'; +import SearchAutocompleteList from './SearchAutocompleteList'; +import SearchInputSelectionWrapper from './SearchInputSelectionWrapper'; import {buildSubstitutionsMap} from './SearchRouter/buildSubstitutionsMap'; import {getQueryWithSubstitutions} from './SearchRouter/getQueryWithSubstitutions'; import type {SubstitutionMap} from './SearchRouter/getQueryWithSubstitutions'; import {getUpdatedSubstitutionsMap} from './SearchRouter/getUpdatedSubstitutionsMap'; import SearchButton from './SearchRouter/SearchButton'; import {useSearchRouterContext} from './SearchRouter/SearchRouterContext'; -import SearchRouterInput from './SearchRouter/SearchRouterInput'; -import SearchRouterList from './SearchRouter/SearchRouterList'; import type {SearchQueryJSON, SearchQueryString} from './types'; // When counting absolute positioning, we need to account for borders @@ -79,7 +79,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps // The actual input text that the user sees const [textInputValue, setTextInputValue] = useState(queryText); - // The input text that was last used for autocomplete; needed for the SearchRouterList when browsing list via arrow keys + // The input text that was last used for autocomplete; needed for the SearchAutocompleteList when browsing list via arrow keys const [autocompleteQueryValue, setAutocompleteQueryValue] = useState(queryText); const [selection, setSelection] = useState({start: textInputValue.length, end: textInputValue.length}); @@ -262,7 +262,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps style={[styles.searchResultsHeaderBar, isAutocompleteListVisible && styles.ph3]} > - - ({}); @@ -295,7 +295,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) )} {isRecentSearchesDataLoaded && ( <> - - {}, - routerListRef, - isFullWidth, - disabled = false, - shouldShowOfflineMessage = false, - autoFocus = true, - onFocus, - onBlur, - caretHidden = false, - wrapperStyle, - wrapperFocusedStyle, - outerWrapperStyle, - rightComponent, - isSearchingForReports, - selection, - }: SearchRouterInputProps, - ref: ForwardedRef, -) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const [isFocused, setIsFocused] = useState(false); - const {isOffline} = useNetwork(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - - const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; - - return ( - - - - { - setIsFocused(true); - routerListRef?.current?.updateExternalTextInputFocus(true); - onFocus?.(); - }} - onBlur={() => { - setIsFocused(false); - routerListRef?.current?.updateExternalTextInputFocus(false); - onBlur?.(); - }} - isLoading={!!isSearchingForReports} - ref={ref} - onKeyPress={handleKeyPress(onSubmit)} - isMarkdownEnabled - multiline={false} - parser={(input: string) => parseForLiveMarkdown(input, currentUserPersonalDetails.login ?? '', currentUserPersonalDetails.displayName ?? '')} - selection={selection} - /> - - {!!rightComponent && {rightComponent}} - - - - ); -} - -SearchRouterInput.displayName = 'SearchRouterInput'; - -export default forwardRef(SearchRouterInput); diff --git a/src/components/Search/SearchRouter/SearchRouterInput/types.ts b/src/components/Search/SearchRouter/SearchRouterInput/types.ts deleted file mode 100644 index da126c8085eaf..0000000000000 --- a/src/components/Search/SearchRouter/SearchRouterInput/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type {ReactNode, RefObject} from 'react'; -import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; -import type {SelectionListHandle} from '@components/SelectionList/types'; - -type SearchRouterInputProps = { - /** Value of TextInput */ - value: string; - - /** Callback to update search in SearchRouter */ - onSearchQueryChange: (searchTerm: string) => void; - - /** Callback invoked when the user submits the input */ - onSubmit?: () => void; - - /** SearchRouterList ref for managing TextInput and SearchRouterList focus */ - routerListRef?: RefObject; - - /** Whether the input is full width */ - isFullWidth: boolean; - - /** Whether the input is disabled */ - disabled?: boolean; - - /** Whether the offline message should be shown */ - shouldShowOfflineMessage?: boolean; - - /** Callback to call when the input gets focus */ - onFocus?: () => void; - - /** Callback to call when the input gets blur */ - onBlur?: () => void; - - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; - - /** Any additional styles to apply when input is focused */ - wrapperFocusedStyle?: StyleProp; - - /** Any additional styles to apply to text input along with FormHelperMessage */ - outerWrapperStyle?: StyleProp; - - /** Component to be displayed on the right */ - rightComponent?: ReactNode; - - /** Whether the search reports API call is running */ - isSearchingForReports?: boolean; -} & Pick; - -export default SearchRouterInputProps; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 19c47414b0898..fbffaf1b82699 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -12,7 +12,7 @@ import type { ViewStyle, } from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; -import type {SearchRouterItem} from '@components/Search/SearchRouter/SearchRouterList'; +import type {SearchRouterItem} from '@components/Search/SearchAutocompleteList'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; // eslint-disable-next-line no-restricted-imports import type CursorStyles from '@styles/utils/cursor/types'; diff --git a/tests/perf-test/SearchRouter.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx index 0784813127bed..1253179850f9d 100644 --- a/tests/perf-test/SearchRouter.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -6,8 +6,8 @@ import Onyx from 'react-native-onyx'; import {measureRenders} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import {OptionsListContext} from '@components/OptionListContextProvider'; +import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput'; import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; -import SearchRouterInput from '@components/Search/SearchRouter/SearchRouterInput'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; import {createOptionList} from '@libs/OptionsListUtils'; import ComposeProviders from '@src/components/ComposeProviders'; @@ -127,11 +127,11 @@ afterEach(() => { const mockOnClose = jest.fn(); -function SearchRouterInputWrapper() { +function SearchAutocompleteInputWrapper() { const [value, setValue] = React.useState(''); return ( - setValue(searchTerm)} isFullWidth={false} @@ -169,7 +169,7 @@ test('[SearchRouter] should render list with cached options', async () => { test('[SearchRouter] should react to text input changes', async () => { const scenario = async () => { - const input = await screen.findByTestId('search-router-text-input'); + const input = await screen.findByTestId('search-autocomplete-text-input'); fireEvent.changeText(input, 'Email Four'); fireEvent.changeText(input, 'Report'); fireEvent.changeText(input, 'Email Five'); @@ -184,5 +184,5 @@ test('[SearchRouter] should react to text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => measureRenders(, {scenario})); + .then(() => measureRenders(, {scenario})); }); From 4347c8d126205aabd04a208c38fcb35a1cb5b512 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Tue, 14 Jan 2025 11:16:24 +0100 Subject: [PATCH 11/19] fix failing parser tests --- src/libs/SearchParser/searchParser.js | 13 +++++++++++-- src/libs/SearchParser/searchParser.peggy | 10 ++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 45b5c290198eb..804a5928a9095 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -311,10 +311,19 @@ 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("") + // return buildFilter("eq", "keyword", value.join("")); + }else{ + word = value } - return buildFilter("eq", "keyword", value); + if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { + return buildFilter("eq", "keyword", word.slice(1, -1)); + } + return buildFilter("eq", "keyword", word); + + // return buildFilter("eq", "keyword", value); }; 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 768daa51a1119..5bb2f3e9ccb52 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -85,10 +85,16 @@ defaultFilter freeTextFilter = _ 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); + if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { + return buildFilter("eq", "keyword", word.slice(1, -1)); + } + return buildFilter("eq", "keyword", word); } standardFilter From 0c52b0fa3e24150abd629c12206d9c33e0962893 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 17 Jan 2025 11:59:08 +0100 Subject: [PATCH 12/19] fix: changed files lint --- .../Search/SearchRouter/SearchRouter.tsx | 24 +++++++++---------- src/libs/SearchAutocompleteUtils.ts | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 010650cb4e5aa..b305f038b5647 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -20,14 +20,14 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as InputUtils from '@libs/InputUtils'; +import {scrollToRight} from '@libs/InputUtils'; import type {SearchOption} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; -import * as SearchAutocompleteUtils from '@libs/SearchAutocompleteUtils'; -import * as SearchQueryUtils from '@libs/SearchQueryUtils'; +import {getAutocompleteQueryWithComma, getQueryWithoutAutocompletedPart} from '@libs/SearchAutocompleteUtils'; +import {getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; -import * as ReportUserActions from '@userActions/Report'; +import {navigateToAndOpenReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -55,12 +55,12 @@ function getContextualSearchQuery(item: SearchQueryItem) { case CONST.SEARCH.DATA_TYPES.INVOICE: additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${item.policyID}`; if (item.roomType === CONST.SEARCH.DATA_TYPES.INVOICE && item.autocompleteID) { - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${SearchQueryUtils.sanitizeSearchValue(item.searchQuery ?? '')}`; + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue(item.searchQuery ?? '')}`; } break; case CONST.SEARCH.DATA_TYPES.CHAT: default: - additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${SearchQueryUtils.sanitizeSearchValue(item.searchQuery ?? '')}`; + additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '')}`; break; } return baseQuery + additionalQuery; @@ -171,7 +171,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) return; } - InputUtils.scrollToRight(textInputRef.current); + scrollToRight(textInputRef.current); shouldScrollRef.current = false; }, []); @@ -180,7 +180,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) if (autoScrollToRight) { shouldScrollRef.current = true; } - const updatedUserQuery = SearchAutocompleteUtils.getAutocompleteQueryWithComma(textInputValue, userQuery); + const updatedUserQuery = getAutocompleteQueryWithComma(textInputValue, userQuery); setTextInputValue(updatedUserQuery); setAutocompleteQueryValue(updatedUserQuery); @@ -199,7 +199,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) const submitSearch = useCallback( (queryString: SearchQueryString) => { const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions); - const updatedQuery = SearchQueryUtils.getQueryWithUpdatedValues(queryWithSubstitutions, activeWorkspaceID); + const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, activeWorkspaceID); if (!updatedQuery) { return; } @@ -241,8 +241,8 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) setAutocompleteSubstitutions(substitutions); } } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) { - const trimmedUserSearchQuery = SearchAutocompleteUtils.getQueryWithoutAutocompletedPart(textInputValue); - const newSearchQuery = `${trimmedUserSearchQuery}${SearchQueryUtils.sanitizeSearchValue(item.searchQuery)}\u00A0`; + const trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(textInputValue); + const newSearchQuery = `${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)}\u00A0`; onSearchQueryChange(newSearchQuery); setSelection({start: newSearchQuery.length, end: newSearchQuery.length}); @@ -262,7 +262,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps) if (item?.reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); } else if ('login' in item) { - ReportUserActions.navigateToAndOpenReport(item.login ? [item.login] : [], false); + navigateToAndOpenReport(item.login ? [item.login] : [], false); } } }, diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 162c5e27fd8e2..b1bfca4547849 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -5,7 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags} from '@src/types/onyx'; import {getTagNamesFromTagsLists} from './PolicyUtils'; -import * as autocompleteParser from './SearchParser/autocompleteParser'; +import {parse} from './SearchParser/autocompleteParser'; /** * Parses given query using the autocomplete parser. @@ -13,7 +13,7 @@ import * as autocompleteParser from './SearchParser/autocompleteParser'; */ function parseForAutocomplete(text: string) { try { - const parsedAutocomplete = autocompleteParser.parse(text) as SearchAutocompleteResult; + const parsedAutocomplete = parse(text) as SearchAutocompleteResult; return parsedAutocomplete; } catch (e) { console.error(`Error when parsing autocomplete query"`, e); @@ -141,7 +141,7 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { function parseForLiveMarkdown(input: string, userLogin: string, userDisplayName: string) { 'worklet'; - const parsedAutocomplete = autocompleteParser.parse(input) as SearchAutocompleteResult; + const parsedAutocomplete = parse(input) as SearchAutocompleteResult; const ranges = parsedAutocomplete.ranges; return ranges.map((range) => { From 819f3893c28f37fc2a0da4897ebbe7c2b853e445 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 24 Jan 2025 11:15:16 +0100 Subject: [PATCH 13/19] add necessary comments to parser files --- src/libs/SearchParser/autocompleteParser.js | 5 +++++ src/libs/SearchParser/autocompleteParser.peggy | 5 +++++ src/libs/SearchParser/searchParser.js | 9 +++++---- src/libs/SearchParser/searchParser.peggy | 6 +++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 5a787c5d8048d..f4ebc66f828e4 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -6,6 +6,11 @@ class peg$SyntaxError{} // https://peggyjs.org/ +// CAUTION: DO NOT DIRECTLY ALTER OR MODIFY `searchParser.js` OR `autocompleteParser.js` +// These files are auto-generated by Peggy from grammar files (*.peggy). +// To make changes, edit the corresponding *.peggy files only. +// Use the `generate-search-parser` and `generate-autocomplete-parser` scripts to regenerate parsers after modifications. + function peg$subclass(child, parent) { function C() { this.constructor = child; } C.prototype = parent.prototype; diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 928d1751f8ceb..54af9dcf9e816 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -12,6 +12,11 @@ // // filter, logicalAnd, operator, alphanumeric, quotedString are defined in baseRules.peggy grammar. // +{{// CAUTION: DO NOT DIRECTLY ALTER OR MODIFY `searchParser.js` OR `autocompleteParser.js` +// These files are auto-generated by Peggy from grammar files (*.peggy). +// To make changes, edit the corresponding *.peggy files only. +// Use the `generate-search-parser` and `generate-autocomplete-parser` scripts to regenerate parsers after modifications. +}} // per-parser initializer (code executed before every parse). { let autocomplete = null; } diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 804a5928a9095..a9bc00881b11c 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -3,6 +3,10 @@ // https://peggyjs.org/ +// CAUTION: DO NOT DIRECTLY ALTER OR MODIFY `searchParser.js` OR `autocompleteParser.js` +// These files are auto-generated by Peggy from grammar files (*.peggy). +// To make changes, edit the corresponding *.peggy files only. +// Use the `generate-search-parser` and `generate-autocomplete-parser` scripts to regenerate parsers after modifications. function buildFilter(operator, left, right) { return { operator, left, right }; @@ -298,7 +302,7 @@ function peg$parse(input, options) { const keywordFilter = buildFilter( "eq", "keyword", - keywords.map((filter) => filter.right).flat() + keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat() ); if (keywordFilter.right.length > 0) { nonKeywords.push(keywordFilter); @@ -314,7 +318,6 @@ function peg$parse(input, options) { let word if (Array.isArray(value)) { word = value.join("") - // return buildFilter("eq", "keyword", value.join("")); }else{ word = value } @@ -322,8 +325,6 @@ function peg$parse(input, options) { return buildFilter("eq", "keyword", word.slice(1, -1)); } return buildFilter("eq", "keyword", word); - - // return buildFilter("eq", "keyword", value); }; 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 28f5c3d8a96fb..f96836b01ff17 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -16,7 +16,11 @@ // filter, logicalAnd, operator, alphanumeric, quotedStrig are defined in baseRules.peggy grammar // global initializer (code executed only once) -{{ +{{// CAUTION: DO NOT DIRECTLY ALTER OR MODIFY `searchParser.js` OR `autocompleteParser.js` +// These files are auto-generated by Peggy from grammar files (*.peggy). +// To make changes, edit the corresponding *.peggy files only. +// Use the `generate-search-parser` and `generate-autocomplete-parser` scripts to regenerate parsers after modifications. + function buildFilter(operator, left, right) { return { operator, left, right }; } From 8a5342520c780ce5d55d1f553fd34311f2e6566b Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Tue, 28 Jan 2025 14:15:35 +0100 Subject: [PATCH 14/19] fix parser failing tests --- src/libs/SearchParser/searchParser.js | 16 +++++----------- src/libs/SearchParser/searchParser.peggy | 12 +++--------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index a9bc00881b11c..463fe85566a6c 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\xA0]/; + var peg$r0 = /^[^ \t\r\n]/; 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", "\xA0"], true, false); + var peg$e0 = peg$classExpectation([" ", "\t", "\r", "\n"], true, false); var peg$e1 = peg$otherExpectation("key"); var peg$e2 = peg$otherExpectation("default key"); var peg$e3 = peg$literalExpectation(",", false); @@ -314,17 +314,11 @@ function peg$parse(input, options) { var peg$f2 = function(key, op, value) { updateDefaultValues(key, value); }; - var peg$f3 = function(value) { //handle no-breaking space - let word + var peg$f3 = function(value) { if (Array.isArray(value)) { - word = value.join("") - }else{ - word = value + return buildFilter("eq", "keyword", value.join("")); } - if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { - return buildFilter("eq", "keyword", word.slice(1, -1)); - } - return buildFilter("eq", "keyword", word); + return buildFilter("eq", "keyword", value); }; 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 f96836b01ff17..b379f22b59e10 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -88,17 +88,11 @@ defaultFilter } freeTextFilter - = _ value:(quotedString / [^ \t\r\n\xA0]+) _ { //handle no-breaking space - let word + = _ value:(quotedString / [^ \t\r\n]+) _ { if (Array.isArray(value)) { - word = value.join("") - }else{ - word = value + return buildFilter("eq", "keyword", value.join("")); } - if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { - return buildFilter("eq", "keyword", word.slice(1, -1)); - } - return buildFilter("eq", "keyword", word); + return buildFilter("eq", "keyword", value); } standardFilter From 51df8e1a922de977681baddcc1a567418744a091 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Tue, 28 Jan 2025 15:12:28 +0100 Subject: [PATCH 15/19] fix styles --- src/components/Search/SearchAutocompleteInput.tsx | 10 +++------- .../Search/SearchInputSelectionWrapper/index.tsx | 3 --- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 5ab33aed5088b..66dbcd7627161 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -1,7 +1,7 @@ import type {ForwardedRef, ReactNode, RefObject} from 'react'; import React, {forwardRef, useState} from 'react'; import {View} from 'react-native'; -import type {StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; @@ -58,9 +58,6 @@ type SearchAutocompleteInputProps = { /** Whether the search reports API call is running */ isSearchingForReports?: boolean; - - /** input style */ - inputStyle?: StyleProp; } & Pick; function SearchAutocompleteInput( @@ -82,7 +79,6 @@ function SearchAutocompleteInput( rightComponent, isSearchingForReports, selection, - inputStyle, }: SearchAutocompleteInputProps, ref: ForwardedRef, ) { @@ -118,8 +114,8 @@ function SearchAutocompleteInput( maxLength={CONST.SEARCH_QUERY_LIMIT} onSubmitEditing={onSubmit} shouldUseDisabledStyles={false} - textInputContainerStyles={[styles.borderNone, styles.pb0]} - inputStyle={[inputWidth, inputStyle]} + textInputContainerStyles={[styles.borderNone, styles.pb0, styles.pr3]} + inputStyle={[inputWidth, styles.pl3, styles.pr3]} onFocus={() => { setIsFocused(true); autocompleteListRef?.current?.updateExternalTextInputFocus(true); diff --git a/src/components/Search/SearchInputSelectionWrapper/index.tsx b/src/components/Search/SearchInputSelectionWrapper/index.tsx index a806cea9afd45..0dcf3282b032f 100644 --- a/src/components/Search/SearchInputSelectionWrapper/index.tsx +++ b/src/components/Search/SearchInputSelectionWrapper/index.tsx @@ -3,14 +3,11 @@ import React, {forwardRef} from 'react'; import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput'; import type {SearchAutocompleteInputProps} from '@components/Search/SearchAutocompleteInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; -import useThemeStyles from '@hooks/useThemeStyles'; function SearchInputSelectionWrapper({selection, ...props}: SearchAutocompleteInputProps, ref: ForwardedRef) { - const styles = useThemeStyles(); return ( Date: Tue, 28 Jan 2025 15:17:36 +0100 Subject: [PATCH 16/19] fix typescript --- .../Search/SearchInputSelectionWrapper/index.native.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/Search/SearchInputSelectionWrapper/index.native.tsx b/src/components/Search/SearchInputSelectionWrapper/index.native.tsx index 8e6fcca4dcf81..534e6f3824a07 100644 --- a/src/components/Search/SearchInputSelectionWrapper/index.native.tsx +++ b/src/components/Search/SearchInputSelectionWrapper/index.native.tsx @@ -3,13 +3,10 @@ import React, {forwardRef} from 'react'; import SearchAutocompleteInput from '@components/Search/SearchAutocompleteInput'; import type {SearchAutocompleteInputProps} from '@components/Search/SearchAutocompleteInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; -import useThemeStyles from '@hooks/useThemeStyles'; function SearchInputSelectionWrapper(props: SearchAutocompleteInputProps, ref: ForwardedRef) { - const styles = useThemeStyles(); return ( Date: Wed, 29 Jan 2025 11:46:08 +0100 Subject: [PATCH 17/19] add support for all logins --- src/components/Search/SearchAutocompleteInput.tsx | 8 +++++++- src/libs/SearchAutocompleteUtils.ts | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 032ba9b7f5f9f..ae18442745ded 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -2,6 +2,7 @@ import type {ForwardedRef, ReactNode, RefObject} from 'react'; import React, {forwardRef, useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; @@ -15,6 +16,7 @@ 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'; type SearchAutocompleteInputProps = { /** Value of TextInput */ @@ -87,6 +89,10 @@ function SearchAutocompleteInput( const [isFocused, setIsFocused] = useState(false); const {isOffline} = useNetwork(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const emailList = Object.keys(loginList ?? {}); + const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; @@ -137,7 +143,7 @@ function SearchAutocompleteInput( parser={(input: string) => { 'worklet'; - return parseForLiveMarkdown(input, currentUserPersonalDetails.login ?? '', currentUserPersonalDetails.displayName ?? ''); + return parseForLiveMarkdown(input, emailList, currentUserPersonalDetails.displayName ?? ''); }} selection={selection} /> diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index b1bfca4547849..ce157c9e6daf9 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -3,7 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {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'; +import type {LoginList, Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags} from '@src/types/onyx'; import {getTagNamesFromTagsLists} from './PolicyUtils'; import {parse} from './SearchParser/autocompleteParser'; @@ -138,7 +138,7 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { * 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, userLogin: string, userDisplayName: string) { +function parseForLiveMarkdown(input: string, userLogins: string[], userDisplayName: string) { 'worklet'; const parsedAutocomplete = parse(input) as SearchAutocompleteResult; @@ -147,7 +147,7 @@ function parseForLiveMarkdown(input: string, userLogin: string, userDisplayName: return ranges.map((range) => { let type = 'mention-user'; - if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (range.value === userLogin || range.value === userDisplayName)) { + 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'; } From 601aeb9cfe8c98383771085adc7736e2abcfb14b Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Wed, 29 Jan 2025 11:56:31 +0100 Subject: [PATCH 18/19] fix lint --- src/libs/SearchAutocompleteUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index ce157c9e6daf9..2aece1753758c 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -3,7 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SearchAutocompleteResult} from '@components/Search/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {LoginList, Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags} from '@src/types/onyx'; +import type {Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags} from '@src/types/onyx'; import {getTagNamesFromTagsLists} from './PolicyUtils'; import {parse} from './SearchParser/autocompleteParser'; From e86a8ad6145a135f4761c0561c68ee7bcab5dcd7 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 31 Jan 2025 10:55:52 +0100 Subject: [PATCH 19/19] fix names --- src/components/Search/SearchPageHeader.tsx | 2 +- src/components/Search/SearchPageHeaderInput.tsx | 4 ++-- src/styles/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index a36368293cb2b..70398194cc840 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -395,7 +395,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { onTooltipPress={onFiltersButtonPress} >