Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight autocomplete value #54403

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fa5c8bc
add script for parser workletization
289Adam289 Dec 19, 2024
cc24f25
update parser
289Adam289 Dec 19, 2024
8a7ea58
Improve parsers - handle NBSP
289Adam289 Dec 20, 2024
5d38a05
improve sanitize string and and workletized parser function
289Adam289 Dec 20, 2024
44c8aae
Migrate SearchRouterInput to RNMarkdownTextInput
289Adam289 Dec 20, 2024
b5d82de
fix script lint
289Adam289 Dec 20, 2024
2a65f02
fix shell lint and add styles
289Adam289 Dec 20, 2024
e320a65
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 7, 2025
9759ee9
fix lint on chnaged files
289Adam289 Jan 8, 2025
95c2690
changes current user mention color
289Adam289 Jan 8, 2025
9a243d2
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 8, 2025
c870653
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 10, 2025
9b80cd0
logic separation and names refactor
289Adam289 Jan 13, 2025
226eb60
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 14, 2025
4347c8d
fix failing parser tests
289Adam289 Jan 14, 2025
9197506
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 17, 2025
0c52b0f
fix: changed files lint
289Adam289 Jan 17, 2025
49ec771
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 20, 2025
e95cd01
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 23, 2025
514da4e
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 24, 2025
819f389
add necessary comments to parser files
289Adam289 Jan 24, 2025
f6c73f6
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 28, 2025
8a53425
fix parser failing tests
289Adam289 Jan 28, 2025
257e060
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 28, 2025
51df8e1
fix styles
289Adam289 Jan 28, 2025
ced2f32
fix typescript
289Adam289 Jan 28, 2025
83a963e
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 28, 2025
afe82b9
add support for all logins
289Adam289 Jan 29, 2025
601aeb9
fix lint
289Adam289 Jan 29, 2025
8f23199
Merge branch 'main' into 289Adam289/50949-highlight-autocomplete
289Adam289 Jan 31, 2025
e86a8ad
fix names
289Adam289 Jan 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,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": {
Expand Down
22 changes: 22 additions & 0 deletions scripts/parser-workletization.sh
Original file line number Diff line number Diff line change
@@ -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 some parts of 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
# 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"
else
echo "An error occurred while modifying the file."
rm -f tmp.txt
exit 1
fi
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import type {ForwardedRef, ReactNode, RefObject} from 'react';
import React, {forwardRef, useState} from 'react';
import type {StyleProp, TextInputProps, ViewStyle} from 'react-native';
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';
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 {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';

type SearchRouterInputProps = {
type SearchAutocompleteInputProps = {
/** Value of TextInput */
value: string;

Expand All @@ -24,8 +28,8 @@ type SearchRouterInputProps = {
/** Callback invoked when the user submits the input */
onSubmit?: () => void;

/** SearchRouterList ref for managing TextInput and SearchRouterList focus */
routerListRef?: RefObject<SelectionListHandle>;
/** SearchAutocompleteList ref for managing TextInput and SearchAutocompleteList focus */
autocompleteListRef?: RefObject<SelectionListHandle>;

/** Whether the input is full width */
isFullWidth: boolean;
Expand Down Expand Up @@ -56,14 +60,14 @@ type SearchRouterInputProps = {

/** Whether the search reports API call is running */
isSearchingForReports?: boolean;
} & Pick<TextInputProps, 'caretHidden' | 'autoFocus'>;
} & Pick<TextInputProps, 'caretHidden' | 'autoFocus' | 'selection'>;

function SearchRouterInput(
function SearchAutocompleteInput(
{
value,
onSearchQueryChange,
onSubmit = () => {},
routerListRef,
autocompleteListRef,
isFullWidth,
disabled = false,
shouldShowOfflineMessage = false,
Expand All @@ -76,13 +80,19 @@ function SearchRouterInput(
outerWrapperStyle,
rightComponent,
isSearchingForReports,
}: SearchRouterInputProps,
selection,
}: SearchAutocompleteInputProps,
ref: ForwardedRef<BaseTextInputRef>,
) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [isFocused, setIsFocused] = useState<boolean>(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};
Expand All @@ -95,7 +105,7 @@ function SearchRouterInput(
fsClass="fs-unmask"
>
<TextInput
testID="search-router-text-input"
testID="search-autocomplete-text-input"
value={value}
onChangeText={onSearchQueryChange}
autoFocus={autoFocus}
Expand All @@ -113,21 +123,29 @@ function SearchRouterInput(
maxLength={CONST.SEARCH_QUERY_LIMIT}
onSubmitEditing={onSubmit}
shouldUseDisabledStyles={false}
textInputContainerStyles={[styles.borderNone, styles.pb0]}
textInputContainerStyles={[styles.borderNone, styles.pb0, styles.pr3]}
inputStyle={[inputWidth, styles.pl3, styles.pr3]}
onFocus={() => {
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}
ref={ref}
onKeyPress={handleKeyPress(onSubmit)}
isMarkdownEnabled
multiline={false}
parser={(input: string) => {
'worklet';

return parseForLiveMarkdown(input, emailList, currentUserPersonalDetails.displayName ?? '');
}}
selection={selection}
/>
</View>
{!!rightComponent && <View style={styles.pr3}>{rightComponent}</View>}
Expand All @@ -141,6 +159,7 @@ function SearchRouterInput(
);
}

SearchRouterInput.displayName = 'SearchRouterInput';
SearchAutocompleteInput.displayName = 'SearchAutocompleteInput';

export default forwardRef(SearchRouterInput);
export type {SearchAutocompleteInputProps};
export default forwardRef(SearchAutocompleteInput);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -49,7 +49,7 @@ type AutocompleteItemData = {

type GetAdditionalSectionsCallback = (options: Options) => Array<SectionListDataType<OptionData | SearchQueryItem>> | undefined;

type SearchRouterListProps = {
type SearchAutocompleteListProps = {
/** Value of TextInput */
autocompleteQueryValue: string;

Expand Down Expand Up @@ -117,9 +117,8 @@ function SearchRouterItem(props: UserListItemProps<OptionData> | SearchQueryList
);
}

// Todo rename to SearchAutocompleteList once it's used in both Router and SearchPage
function SearchRouterList(
{autocompleteQueryValue, searchQueryItem, getAdditionalSections, onListItemPress, setTextQuery, updateAutocompleteSubstitutions}: SearchRouterListProps,
function SearchAutocompleteList(
{autocompleteQueryValue, searchQueryItem, getAdditionalSections, onListItemPress, setTextQuery, updateAutocompleteSubstitutions}: SearchAutocompleteListProps,
ref: ForwardedRef<SelectionListHandle>,
) {
const styles = useThemeStyles();
Expand Down Expand Up @@ -465,7 +464,7 @@ function SearchRouterList(
}

const trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(autocompleteQueryValue);
setTextQuery(`${trimmedUserSearchQuery}${sanitizeSearchValue(focusedItem.searchQuery)} `);
setTextQuery(`${trimmedUserSearchQuery}${sanitizeSearchValue(focusedItem.searchQuery)}\u00A0`);
updateAutocompleteSubstitutions(focusedItem);
},
[autocompleteQueryValue, setTextQuery, updateAutocompleteSubstitutions],
Expand Down Expand Up @@ -495,6 +494,6 @@ function SearchRouterList(
);
}

export default forwardRef(SearchRouterList);
export default forwardRef(SearchAutocompleteList);
export {SearchRouterItem};
export type {GetAdditionalSectionsCallback};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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';

function SearchInputSelectionWrapper(props: SearchAutocompleteInputProps, ref: ForwardedRef<BaseTextInputRef>) {
return (
<SearchAutocompleteInput
ref={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
selection={undefined}
/>
);
}

SearchInputSelectionWrapper.displayName = 'SearchInputSelectionWrapper';

export default forwardRef(SearchInputSelectionWrapper);
20 changes: 20 additions & 0 deletions src/components/Search/SearchInputSelectionWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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';

function SearchInputSelectionWrapper({selection, ...props}: SearchAutocompleteInputProps, ref: ForwardedRef<BaseTextInputRef>) {
return (
<SearchAutocompleteInput
selection={selection}
ref={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

SearchInputSelectionWrapper.displayName = 'SearchInputSelectionWrapper';

export default forwardRef(SearchInputSelectionWrapper);
2 changes: 1 addition & 1 deletion src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) {
onTooltipPress={onFiltersButtonPress}
>
<Button
innerStyles={!isCannedQuery && [styles.searchRouterInputResults, styles.borderNone]}
innerStyles={!isCannedQuery && [styles.searchAutocompleteInputResults, styles.borderNone]}
text={translate('search.filtersHeader')}
icon={Expensicons.Filters}
onPress={onFiltersButtonPress}
Expand Down
32 changes: 22 additions & 10 deletions src/components/Search/SearchPageHeaderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,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
Expand Down Expand Up @@ -83,8 +83,9 @@ 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});

const [autocompleteSubstitutions, setAutocompleteSubstitutions] = useState<SubstitutionMap>({});
const [isAutocompleteListVisible, setIsAutocompleteListVisible] = useState(false);
Expand Down Expand Up @@ -165,7 +166,9 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps

if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
const trimmedUserSearchQuery = getQueryWithoutAutocompletedPart(textInputValue);
onSearchQueryChange(`${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)} `);
const newSearchQuery = `${trimmedUserSearchQuery}${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};
Expand Down Expand Up @@ -196,6 +199,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;

Expand Down Expand Up @@ -258,7 +269,7 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
style={[styles.searchResultsHeaderBar, isAutocompleteListVisible && styles.ph3]}
>
<View style={[styles.appBG, ...autocompleteInputStyle]}>
<SearchRouterInput
<SearchInputSelectionWrapper
value={textInputValue}
onSearchQueryChange={onSearchQueryChange}
isFullWidth
Expand All @@ -272,19 +283,20 @@ function SearchPageHeaderInput({queryJSON, children}: SearchPageHeaderInputProps
autoFocus={false}
onFocus={showAutocompleteList}
onBlur={hideAutocompleteList}
wrapperStyle={[styles.searchRouterInputResults, styles.br2]}
wrapperFocusedStyle={styles.searchRouterInputResultsFocused}
wrapperStyle={[styles.searchAutocompleteInputResults, styles.br2]}
wrapperFocusedStyle={styles.searchAutocompleteInputResultsFocused}
outerWrapperStyle={[inputWrapperActiveStyle, styles.pb2]}
rightComponent={children}
routerListRef={listRef}
autocompleteListRef={listRef}
ref={textInputRef}
selection={selection}
/>
<View style={[styles.mh85vh, !isAutocompleteListVisible && styles.dNone]}>
<SearchRouterList
<SearchAutocompleteList
autocompleteQueryValue={autocompleteQueryValue}
searchQueryItem={searchQueryItem}
onListItemPress={onListItemPress}
setTextQuery={setTextInputValue}
setTextQuery={setTextAndUpdateSelection}
updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
ref={listRef}
/>
Expand Down
Loading
Loading