From 73d9a10a58b9fc5cda0dd6479f4acf635721f965 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 7 Feb 2024 20:48:26 +0500 Subject: [PATCH 01/16] feat: add delete option to deleteable report fields --- .../API/parameters/DeleteReportFieldParams.ts | 6 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Report.ts | 58 +++++++++++++++++++ src/pages/EditReportFieldDatePage.tsx | 11 +++- src/pages/EditReportFieldDropdownPage.tsx | 11 +++- src/pages/EditReportFieldPage.tsx | 20 +++++++ src/pages/EditReportFieldTextPage.tsx | 11 +++- 8 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/libs/API/parameters/DeleteReportFieldParams.ts diff --git a/src/libs/API/parameters/DeleteReportFieldParams.ts b/src/libs/API/parameters/DeleteReportFieldParams.ts new file mode 100644 index 000000000000..393c21af0088 --- /dev/null +++ b/src/libs/API/parameters/DeleteReportFieldParams.ts @@ -0,0 +1,6 @@ +type DeleteReportFieldParams = { + reportID: string; + reportFields: string; +}; + +export default DeleteReportFieldParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b7c3dff7c342..dba006979dec 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -125,3 +125,4 @@ export type {default as CompleteEngagementModalParams} from './CompleteEngagemen export type {default as SetNameValuePairParams} from './SetNameValuePairParams'; export type {default as SetReportFieldParams} from './SetReportFieldParams'; export type {default as SetReportNameParams} from './SetReportNameParams'; +export type {default as DeleteReportFieldParams} from './DeleteReportFieldParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c011fa395f0f..5c3581a1a6ac 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -115,6 +115,7 @@ const WRITE_COMMANDS = { COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal', SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', + DELETE_REPORT_FIELD: 'DELETE_ReportFields', SET_REPORT_NAME: 'RenameReport', } as const; @@ -229,6 +230,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; [WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams; [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; + [WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4bff826ceb3a..d8e59232688d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1614,6 +1614,63 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); } +function deleteReportField(reportID: string, reportField: PolicyReportField) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportFields: { + [reportField.fieldID]: null, + }, + pendingFields: { + [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportFields: { + [reportField.fieldID]: reportField, + }, + pendingFields: { + [reportField.fieldID]: null, + }, + errorFields: { + [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + pendingFields: { + [reportField.fieldID]: null, + }, + errorFields: { + [reportField.fieldID]: null, + }, + }, + }, + ]; + + const parameters = { + reportID, + reportFields: JSON.stringify({[reportField.fieldID]: reportField}), + }; + + API.write(WRITE_COMMANDS.DELETE_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); +} + function updateWelcomeMessage(reportID: string, previousValue: string, newValue: string) { // No change needed, navigate back if (previousValue === newValue) { @@ -2884,5 +2941,6 @@ export { clearNewRoomFormError, updateReportField, updateReportName, + deleteReportField, resolveActionableMentionWhisper, }; diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 82659eca62c2..3379f6e5f4c1 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -5,6 +5,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -26,11 +27,14 @@ type EditReportFieldDatePageProps = { /** Flag to indicate if the field can be left blank */ isRequired: boolean; + /** Three dot menu item options */ + menuItems?: ThreeDotsMenuItem[]; + /** Callback to fire when the Save button is pressed */ onSubmit: (form: OnyxFormValuesFields) => void; }; -function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { +function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, menuItems, fieldID}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -55,7 +59,10 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f }} testID={EditReportFieldDatePage.displayName} > - + ) => void; }; @@ -37,7 +41,7 @@ type EditReportFieldDropdownPageOnyxProps = { type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps; -function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { +function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields, menuItems}: EditReportFieldDropdownPageProps) { const [searchValue, setSearchValue] = useState(''); const styles = useThemeStyles(); const {getSafeAreaMargins} = useStyleUtils(); @@ -80,7 +84,10 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, > {({insets}) => ( <> - + { + ReportActions.deleteReportField(report.reportID, reportField); + Navigation.dismissModal(report?.reportID); + }; + const fieldValue = isReportFieldTitle ? report.reportName ?? '' : reportField.value ?? reportField.defaultValue; + const menuItems: ThreeDotsMenuItem[] = []; + + const isReportFieldDeletable = report.reportFields?.deletable; + + if (isReportFieldDeletable) { + menuItems.push({icon: Expensicons.Trashcan, text: translate('common.delete'), onSelected: () => handleReportFieldDelete()}); + } + if (reportField.type === 'text' || isReportFieldTitle) { return ( ); @@ -96,6 +114,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe fieldID={reportField.fieldID} fieldValue={fieldValue} isRequired={!reportField.deletable} + menuItems={menuItems} onSubmit={handleReportFieldChange} /> ); @@ -109,6 +128,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe fieldName={Str.UCFirst(reportField.name)} fieldValue={fieldValue} fieldOptions={reportField.values} + menuItems={menuItems} onSubmit={handleReportFieldChange} /> ); diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index ea9d2d3bed6d..f06ad32e1598 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -4,6 +4,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; @@ -26,11 +27,14 @@ type EditReportFieldTextPageProps = { /** Flag to indicate if the field can be left blank */ isRequired: boolean; + /** Three dot menu item options */ + menuItems?: ThreeDotsMenuItem[]; + /** Callback to fire when the Save button is pressed */ onSubmit: (form: OnyxFormValuesFields) => void; }; -function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID}: EditReportFieldTextPageProps) { +function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID, menuItems}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -55,7 +59,10 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f }} testID={EditReportFieldTextPage.displayName} > - + Date: Mon, 19 Feb 2024 05:21:41 +0500 Subject: [PATCH 02/16] prettier --- src/pages/EditReportFieldPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 4f28bd9c4f7a..16badafebe07 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -3,9 +3,9 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import type {FormOnyxValues} from '@components/Form/types'; import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import * as Expensicons from '@components/Icon/Expensicons'; -import type {FormOnyxValues} from '@components/Form/types'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; From ba800ad840b25e1e54d2774b8e5a18e86b187efa Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 19 Feb 2024 06:08:43 +0500 Subject: [PATCH 03/16] more general improvements --- .../ReportActionItem/MoneyReportView.tsx | 2 ++ .../API/parameters/DeleteReportFieldParams.ts | 3 +-- src/libs/API/types.ts | 2 +- src/libs/actions/Report.ts | 15 +++++++++++++-- src/pages/EditReportFieldDatePage.tsx | 4 ++++ src/pages/EditReportFieldDropdownPage.tsx | 4 ++++ src/pages/EditReportFieldPage.tsx | 2 +- src/pages/EditReportFieldTextPage.tsx | 4 ++++ src/pages/reportPropTypes.js | 2 +- 9 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index f0cd8dc1b4b5..61fdc46a623a 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -16,6 +16,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as reportActions from '@src/libs/actions/Report'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; import ROUTES from '@src/ROUTES'; @@ -78,6 +79,7 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont pendingAction={report.pendingFields?.[reportField.fieldID]} errors={report.errorFields?.[reportField.fieldID]} errorRowStyles={styles.ph5} + onClose={() => reportActions.clearReportFieldErrors(report.reportID, reportField.fieldID)} key={`menuItem-${reportField.fieldID}`} > (null); @@ -61,6 +63,8 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, m handleReportFieldDelete()}); diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index 14443d4fe337..cb6bf3f7ae6f 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -10,6 +10,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -34,6 +35,7 @@ type EditReportFieldTextPageProps = { }; function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID, menuItems}: EditReportFieldTextPageProps) { + const {windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -61,6 +63,8 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f Date: Mon, 19 Feb 2024 06:40:31 +0500 Subject: [PATCH 04/16] lint fix --- .../ReportActionItem/MoneyReportView.tsx | 65 ++++++++++--------- src/pages/EditReportFieldDropdownPage.tsx | 2 +- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 61fdc46a623a..3bc6a6959c09 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -16,9 +16,9 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; -import * as reportActions from '@src/libs/actions/Report'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; +import * as reportActions from '@src/libs/actions/Report'; import ROUTES from '@src/ROUTES'; import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; @@ -68,38 +68,39 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont - {ReportUtils.reportFieldsEnabled(report) && - sortedPolicyReportFields.map((reportField) => { - const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField); - const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue; - const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy); + {ReportUtils.reportFieldsEnabled(report) + ? sortedPolicyReportFields.map((reportField) => { + const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField); + const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue; + const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy); - return ( - reportActions.clearReportFieldErrors(report.reportID, reportField.fieldID)} - key={`menuItem-${reportField.fieldID}`} - > - Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))} - shouldShowRightIcon - disabled={isFieldDisabled} - wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} - shouldGreyOutWhenDisabled={false} - numberOfLinesTitle={0} - interactive - shouldStackHorizontally={false} - onSecondaryInteraction={() => {}} - hoverAndPressStyle={false} - titleWithTooltips={[]} - /> - - ); - })} + return ( + reportActions.clearReportFieldErrors(report.reportID, reportField.fieldID)} + key={`menuItem-${reportField.fieldID}`} + > + Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))} + shouldShowRightIcon + disabled={isFieldDisabled} + wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} + shouldGreyOutWhenDisabled={false} + numberOfLinesTitle={0} + interactive + shouldStackHorizontally={false} + onSecondaryInteraction={() => {}} + hoverAndPressStyle={false} + titleWithTooltips={[]} + /> + + ); + }) + : null} Date: Sun, 25 Feb 2024 22:54:18 +0500 Subject: [PATCH 05/16] fix: persist delete report fields --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e7dab0340680..3c3e1a1a8539 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1712,7 +1712,7 @@ function deleteReportField(reportID: string, reportField: PolicyReportField) { ]; const parameters = { - fieldID: reportField.fieldID, + fieldID: `expensify_${reportField.fieldID}`, }; API.write(WRITE_COMMANDS.DELETE_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); From 7950537845073ec3d324ee5d9d475742be67ebd7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 27 Mar 2024 01:28:35 +0500 Subject: [PATCH 06/16] more finetunings --- src/libs/actions/Report.ts | 4 +- src/pages/EditReportFieldDatePage.tsx | 2 +- src/pages/EditReportFieldDropdownPage.tsx | 52 +++++++++-------------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f7db7dabaa45..49fca6d8d613 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1684,7 +1684,7 @@ function deleteReportField(reportID: string, reportField: PolicyReportField) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportFields: { + fieldList: { [reportField.fieldID]: null, }, pendingFields: { @@ -1699,7 +1699,7 @@ function deleteReportField(reportID: string, reportField: PolicyReportField) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportFields: { + fieldList: { [reportField.fieldID]: reportField, }, pendingFields: { diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 9017d201e6a1..b7fc8ef72ccd 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -75,7 +75,7 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, m enabledWhenOffline > - + recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]); @@ -133,35 +132,24 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, shouldEnableMaxHeight testID={EditReportFieldDropdownPage.displayName} > - {({insets}) => ( - <> - - ) => - onSubmit({ - [fieldKey]: fieldValue === option.text ? '' : option.text, - }) - } - onChangeText={setSearchValue} - highlightSelectedOptions - isRowMultilineSupported - headerMessage={headerMessage} - /> - - )} + + + onSubmit({ [fieldKey]: !option?.text || fieldValue === option.text ? '' : option.text }) + } + onChangeText={setSearchValue} + headerMessage={headerMessage} + ListItem={RadioListItem} + /> ); } From 9a44a1aaeffcc4e5db0e27d2bd54b3a0e2aeb92e Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 27 Mar 2024 03:51:57 +0500 Subject: [PATCH 07/16] more refactors --- .../ReportActionItem/MoneyReportView.tsx | 2 +- src/languages/en.ts | 4 + src/languages/es.ts | 4 + src/libs/OptionsListUtils.ts | 101 +++++++++++ src/libs/Permissions.ts | 2 +- src/libs/actions/Report.ts | 26 +-- src/pages/EditReportFieldDate.tsx | 75 ++++++++ src/pages/EditReportFieldDatePage.tsx | 98 ----------- src/pages/EditReportFieldDropdown.tsx | 103 +++++++++++ src/pages/EditReportFieldDropdownPage.tsx | 163 ------------------ src/pages/EditReportFieldPage.tsx | 107 +++++++----- src/pages/EditReportFieldText.tsx | 73 ++++++++ src/pages/EditReportFieldTextPage.tsx | 96 ----------- 13 files changed, 443 insertions(+), 411 deletions(-) create mode 100644 src/pages/EditReportFieldDate.tsx delete mode 100644 src/pages/EditReportFieldDatePage.tsx create mode 100644 src/pages/EditReportFieldDropdown.tsx delete mode 100644 src/pages/EditReportFieldDropdownPage.tsx create mode 100644 src/pages/EditReportFieldText.tsx delete mode 100644 src/pages/EditReportFieldTextPage.tsx diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 50dfa55dcec7..384ea077c350 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -81,7 +81,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport errors={report.errorFields?.[fieldKey]} errorRowStyles={styles.ph5} key={`menuItem-${fieldKey}`} - onClose={() => reportActions.clearReportFieldErrors(report.reportID, reportField.fieldID)} + onClose={() => reportActions.clearReportFieldErrors(report.reportID, reportField)} > ; }; @@ -165,6 +168,7 @@ type GetOptions = { categoryOptions: CategoryTreeSection[]; tagOptions: CategorySection[]; taxRatesOptions: CategorySection[]; + policyReportFieldOptions?: CategorySection[] | null; }; type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; @@ -1224,6 +1228,81 @@ function hasEnabledTags(policyTagList: Array return hasEnabledOptions(policyTagValueList); } +/** + * Transforms the provided report field options into option objects. + * + * @param reportFieldOptions - an initial report field options array + */ +function getReportFieldOptions(reportFieldOptions: string[]): Option[] { + return reportFieldOptions.map((name) => ({ + text: name, + keyForList: name, + searchText: name, + tooltipText: name, + isDisabled: false, + })); +} + +/** + * Build the section list for report field options + */ +function getReportFieldOptionsSection(options: string[], recentlyUsedOptions: string[], selectedOptions: Array>, searchInputValue: string) { + const reportFieldOptionsSections = []; + const selectedOptionKeys = selectedOptions.map(({text, keyForList, name}) => text ?? keyForList ?? name ?? '').filter((o) => !!o); + let indexOffset = 0; + + if (searchInputValue) { + const searchOptions = options.filter((option) => option.toLowerCase().includes(searchInputValue.toLowerCase())); + + reportFieldOptionsSections.push({ + // "Search" section + title: '', + shouldShow: true, + indexOffset, + data: getReportFieldOptions(searchOptions), + }); + + return reportFieldOptionsSections; + } + + const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((recentlyUsedOption) => !selectedOptionKeys.includes(recentlyUsedOption)); + const filteredOptions = options.filter((option) => !selectedOptionKeys.includes(option)); + + if (selectedOptionKeys.length) { + reportFieldOptionsSections.push({ + // "Selected" section + title: '', + shouldShow: true, + indexOffset, + data: getReportFieldOptions(selectedOptionKeys), + }); + + indexOffset += selectedOptionKeys.length; + } + + if (filteredRecentlyUsedOptions.length > 0) { + reportFieldOptionsSections.push({ + // "Recent" section + title: Localize.translateLocal('common.recent'), + shouldShow: true, + indexOffset, + data: getReportFieldOptions(filteredRecentlyUsedOptions), + }); + + indexOffset += filteredRecentlyUsedOptions.length; + } + + reportFieldOptionsSections.push({ + // "All" section when items amount more than the threshold + title: Localize.translateLocal('common.all'), + shouldShow: true, + indexOffset, + data: getReportFieldOptions(filteredOptions), + }); + + return reportFieldOptionsSections; +} + /** * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. * @@ -1407,6 +1486,9 @@ function getOptions( includeTaxRates, taxRates, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions = [], + recentlyUsedPolicyReportFieldOptions = [], }: GetOptionsConfig, ): GetOptions { if (includeCategories) { @@ -1451,6 +1533,19 @@ function getOptions( }; } + if (includePolicyReportFieldOptions) { + return { + recentReports: [], + personalDetails: [], + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + policyReportFieldOptions: getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue), + }; + } + if (!isPersonalDetailsReady(personalDetails)) { return { recentReports: [], @@ -1858,6 +1953,9 @@ function getFilteredOptions( includeTaxRates = false, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions: string[] = [], + recentlyUsedPolicyReportFieldOptions: string[] = [], ) { return getOptions(reports, personalDetails, { betas, @@ -1880,6 +1978,9 @@ function getFilteredOptions( includeTaxRates, taxRates, includeSelfDM, + includePolicyReportFieldOptions, + policyReportFieldOptions, + recentlyUsedPolicyReportFieldOptions, }); } diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 0f42737c270c..b4ac278f9678 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -19,7 +19,7 @@ function canUseCommentLinking(betas: OnyxEntry): boolean { } function canUseReportFields(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); + return true; // !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); } function canUseViolations(betas: OnyxEntry): boolean { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 49fca6d8d613..c189047cf39c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1587,13 +1587,14 @@ function updateReportName(reportID: string, value: string, previousValue: string API.write(WRITE_COMMANDS.SET_REPORT_NAME, parameters, {optimisticData, failureData, successData}); } -function clearReportFieldErrors(reportID: string, reportFieldID: string) { +function clearReportFieldErrors(reportID: string, reportField: PolicyReportField) { + const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID); Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { pendingFields: { - [reportFieldID]: null, + [fieldKey]: null, }, errorFields: { - [reportFieldID]: null, + [fieldKey]: null, }, }); } @@ -1679,16 +1680,18 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre } function deleteReportField(reportID: string, reportField: PolicyReportField) { + const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [reportField.fieldID]: null, + [fieldKey]: null, }, pendingFields: { - [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + [fieldKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, }, @@ -1700,13 +1703,13 @@ function deleteReportField(reportID: string, reportField: PolicyReportField) { key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [reportField.fieldID]: reportField, + [fieldKey]: reportField, }, pendingFields: { - [reportField.fieldID]: null, + [fieldKey]: null, }, errorFields: { - [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), + [fieldKey]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), }, }, }, @@ -1718,17 +1721,18 @@ function deleteReportField(reportID: string, reportField: PolicyReportField) { key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { pendingFields: { - [reportField.fieldID]: null, + [fieldKey]: null, }, errorFields: { - [reportField.fieldID]: null, + [fieldKey]: null, }, }, }, ]; const parameters = { - fieldID: `expensify_${reportField.fieldID}`, + reportID, + fieldID: fieldKey, }; API.write(WRITE_COMMANDS.DELETE_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); diff --git a/src/pages/EditReportFieldDate.tsx b/src/pages/EditReportFieldDate.tsx new file mode 100644 index 000000000000..e7021f9123d6 --- /dev/null +++ b/src/pages/EditReportFieldDate.tsx @@ -0,0 +1,75 @@ +import React, {useCallback, useRef} from 'react'; +import {View} from 'react-native'; +import DatePicker from '@components/DatePicker'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type EditReportFieldDatePageProps = { + /** Value of the policy report field */ + fieldValue: string; + + /** Name of the policy report field */ + fieldName: string; + + /** Key of the policy report field */ + fieldKey: string; + + /** Flag to indicate if the field can be left blank */ + isRequired: boolean; + + /** Callback to fire when the Save button is pressed */ + onSubmit: (form: FormOnyxValues) => void; +}; + +function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldKey}: EditReportFieldDatePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const inputRef = useRef(null); + + const validate = useCallback( + (value: FormOnyxValues) => { + const errors: FormInputErrors = {}; + if (isRequired && value[fieldKey].trim() === '') { + errors[fieldKey] = 'common.error.fieldRequired'; + } + return errors; + }, + [fieldKey, isRequired], + ); + + return ( + + + + + + ); +} + +EditReportFieldDatePage.displayName = 'EditReportFieldDatePage'; + +export default EditReportFieldDatePage; diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx deleted file mode 100644 index b7fc8ef72ccd..000000000000 --- a/src/pages/EditReportFieldDatePage.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, {useCallback, useRef} from 'react'; -import {View} from 'react-native'; -import DatePicker from '@components/DatePicker'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -type EditReportFieldDatePageProps = { - /** Value of the policy report field */ - fieldValue: string; - - /** Name of the policy report field */ - fieldName: string; - - /** Key of the policy report field */ - fieldKey: string; - - /** Flag to indicate if the field can be left blank */ - isRequired: boolean; - - /** Three dot menu item options */ - menuItems?: ThreeDotsMenuItem[]; - - /** Callback to fire when the Save button is pressed */ - onSubmit: (form: FormOnyxValues) => void; -}; - -function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, menuItems, fieldKey}: EditReportFieldDatePageProps) { - const {windowWidth} = useWindowDimensions(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const inputRef = useRef(null); - - const validate = useCallback( - (value: FormOnyxValues) => { - const errors: FormInputErrors = {}; - if (isRequired && value[fieldKey].trim() === '') { - errors[fieldKey] = 'common.error.fieldRequired'; - } - return errors; - }, - [fieldKey, isRequired], - ); - - return ( - { - inputRef.current?.focus(); - }} - testID={EditReportFieldDatePage.displayName} - > - - - - - - - - ); -} - -EditReportFieldDatePage.displayName = 'EditReportFieldDatePage'; - -export default EditReportFieldDatePage; diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx new file mode 100644 index 000000000000..1d0247d0e3de --- /dev/null +++ b/src/pages/EditReportFieldDropdown.tsx @@ -0,0 +1,103 @@ +import React, {useMemo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {RecentlyUsedReportFields} from '@src/types/onyx'; + +type EditReportFieldDropdownPageComponentProps = { + /** Value of the policy report field */ + fieldValue: string; + + /** Key of the policy report field */ + fieldKey: string; + + /** ID of the policy this report field belongs to */ + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; + + /** Options of the policy report field */ + fieldOptions: string[]; + + /** Callback to fire when the Save button is pressed */ + onSubmit: (form: Record) => void; +}; + +type EditReportFieldDropdownPageOnyxProps = { + recentlyUsedReportFields: OnyxEntry; +}; + +type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps; + +function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {translate} = useLocalize(); + const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]); + + const [sections, headerMessage] = useMemo(() => { + const validFieldOptions = fieldOptions?.filter((option) => !!option); + + const {policyReportFieldOptions} = OptionsListUtils.getFilteredOptions( + {}, + {}, + [], + debouncedSearchValue, + [ + { + keyForList: fieldValue, + searchText: fieldValue, + text: fieldValue, + }, + ], + [], + false, + false, + false, + {}, + [], + false, + {}, + [], + false, + false, + undefined, + undefined, + undefined, + true, + validFieldOptions, + recentlyUsedOptions, + ); + + const policyReportFieldData = policyReportFieldOptions?.[0]?.data ?? []; + const header = OptionsListUtils.getHeaderMessageForNonUserList(policyReportFieldData.length > 0, debouncedSearchValue); + + return [policyReportFieldOptions, header]; + }, [recentlyUsedOptions, debouncedSearchValue, fieldValue, fieldOptions]); + + const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((option) => option.searchText === fieldValue)?.[0]?.keyForList, [sections, fieldValue]); + return ( + onSubmit({[fieldKey]: !option?.text || fieldValue === option.text ? '' : option.text})} + initiallyFocusedOptionKey={selectedOptionKey ?? undefined} + onChangeText={setSearchValue} + headerMessage={headerMessage} + ListItem={RadioListItem} + isRowMultilineSupported + /> + ); +} + +EditReportFieldDropdownPage.displayName = 'EditReportFieldDropdownPage'; + +export default withOnyx({ + recentlyUsedReportFields: { + key: () => ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, + }, +})(EditReportFieldDropdownPage); diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx deleted file mode 100644 index 5a81535ac597..000000000000 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, {useMemo, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {RecentlyUsedReportFields} from '@src/types/onyx'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; - -type EditReportFieldDropdownPageComponentProps = { - /** Value of the policy report field */ - fieldValue: string; - - /** Name of the policy report field */ - fieldName: string; - - /** Key of the policy report field */ - fieldKey: string; - - /** ID of the policy this report field belongs to */ - // eslint-disable-next-line react/no-unused-prop-types - policyID: string; - - /** Options of the policy report field */ - fieldOptions: string[]; - - /** Three dot menu item options */ - menuItems?: ThreeDotsMenuItem[]; - - /** Callback to fire when the Save button is pressed */ - onSubmit: (form: Record) => void; -}; - -type EditReportFieldDropdownPageOnyxProps = { - recentlyUsedReportFields: OnyxEntry; -}; - -type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps; - -type ReportFieldDropdownData = { - text: string; - keyForList: string; - searchText: string; - tooltipText: string; -}; - -type ReportFieldDropdownSectionItem = { - data: ReportFieldDropdownData[]; - shouldShow: boolean; - title?: string; -}; - -function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, fieldOptions, menuItems, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { - const {windowWidth} = useWindowDimensions(); - const [searchValue, setSearchValue] = useState(''); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]); - - const {sections, headerMessage} = useMemo(() => { - let newHeaderMessage = ''; - const newSections: ReportFieldDropdownSectionItem[] = []; - - if (searchValue) { - const filteredOptions = fieldOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase())); - newHeaderMessage = !filteredOptions.length ? translate('common.noResultsFound') : ''; - newSections.push({ - shouldShow: false, - data: filteredOptions.map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), - }); - } else { - const selectedValue = fieldValue; - if (selectedValue) { - newSections.push({ - shouldShow: false, - data: [ - { - text: selectedValue, - keyForList: selectedValue, - searchText: selectedValue, - tooltipText: selectedValue, - }, - ], - }); - } - - const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((option) => option !== selectedValue && fieldOptions.includes(option)); - if (filteredRecentlyUsedOptions.length > 0) { - newSections.push({ - title: translate('common.recents'), - shouldShow: true, - data: filteredRecentlyUsedOptions.map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), - }); - } - - const filteredFieldOptions = fieldOptions.filter((option) => option !== selectedValue); - if (filteredFieldOptions.length > 0) { - newSections.push({ - title: translate('common.all'), - shouldShow: true, - data: filteredFieldOptions.map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), - }); - } - } - - return {sections: newSections, headerMessage: newHeaderMessage}; - }, [fieldValue, fieldOptions, recentlyUsedOptions, searchValue, translate]); - - return ( - - - - onSubmit({ [fieldKey]: !option?.text || fieldValue === option.text ? '' : option.text }) - } - onChangeText={setSearchValue} - headerMessage={headerMessage} - ListItem={RadioListItem} - /> - - ); -} - -EditReportFieldDropdownPage.displayName = 'EditReportFieldDropdownPage'; - -export default withOnyx({ - recentlyUsedReportFields: { - key: () => ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, - }, -})(EditReportFieldDropdownPage); diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index fb207213b90e..6cc93d05ebbc 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -1,21 +1,25 @@ import Str from 'expensify-common/lib/str'; -import React from 'react'; +import React, {useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import ConfirmModal from '@components/ConfirmModal'; import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActions from '@src/libs/actions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report} from '@src/types/onyx'; -import EditReportFieldDatePage from './EditReportFieldDatePage'; -import EditReportFieldDropdownPage from './EditReportFieldDropdownPage'; -import EditReportFieldTextPage from './EditReportFieldTextPage'; +import EditReportFieldDate from './EditReportFieldDate'; +import EditReportFieldDropdown from './EditReportFieldDropdown'; +import EditReportFieldText from './EditReportFieldText'; type EditReportFieldPageOnyxProps = { /** The report object for the expense report */ @@ -43,9 +47,12 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { + const {windowWidth} = useWindowDimensions(); + const styles = useThemeStyles(); const fieldKey = ReportUtils.getReportFieldKey(route.params.fieldID); const reportField = report?.fieldList?.[fieldKey] ?? policy?.fieldList?.[fieldKey]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const {translate} = useLocalize(); if (!reportField || !report || isDisabled) { @@ -86,51 +93,69 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) const menuItems: ThreeDotsMenuItem[] = []; - const isReportFieldDeletable = reportField.deletable; + const isReportFieldDeletable = reportField.deletable && !isReportFieldTitle; if (isReportFieldDeletable) { - menuItems.push({icon: Expensicons.Trashcan, text: translate('common.delete'), onSelected: () => handleReportFieldDelete()}); + menuItems.push({icon: Expensicons.Trashcan, text: translate('common.delete'), onSelected: () => setIsDeleteModalVisible(true)}); } - if (reportField.type === 'text' || isReportFieldTitle) { - return ( - + - ); - } - if (reportField.type === 'date') { - return ( - setIsDeleteModalVisible(false)} + prompt={translate('workspace.reportFields.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger /> - ); - } - if (reportField.type === 'dropdown') { - return ( - !reportField.disabledOptions[index])} - menuItems={menuItems} - onSubmit={handleReportFieldChange} - /> - ); - } + {(reportField.type === 'text' || isReportFieldTitle) && ( + + )} + + {reportField.type === 'date' && ( + + )} + + {reportField.type === 'dropdown' && ( + !reportField.disabledOptions[index])} + onSubmit={handleReportFieldChange} + /> + )} + + ); } EditReportFieldPage.displayName = 'EditReportFieldPage'; diff --git a/src/pages/EditReportFieldText.tsx b/src/pages/EditReportFieldText.tsx new file mode 100644 index 000000000000..d89724f0228b --- /dev/null +++ b/src/pages/EditReportFieldText.tsx @@ -0,0 +1,73 @@ +import React, {useCallback, useRef} from 'react'; +import {View} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type EditReportFieldTextPageProps = { + /** Value of the policy report field */ + fieldValue: string; + + /** Name of the policy report field */ + fieldName: string; + + /** Key of the policy report field */ + fieldKey: string; + + /** Flag to indicate if the field can be left blank */ + isRequired: boolean; + + /** Callback to fire when the Save button is pressed */ + onSubmit: (form: FormOnyxValues) => void; +}; + +function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldKey}: EditReportFieldTextPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const inputRef = useRef(null); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + if (isRequired && values[fieldKey].trim() === '') { + errors[fieldKey] = 'common.error.fieldRequired'; + } + return errors; + }, + [fieldKey, isRequired], + ); + + return ( + + + + + + ); +} + +EditReportFieldTextPage.displayName = 'EditReportFieldTextPage'; + +export default EditReportFieldTextPage; diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx deleted file mode 100644 index 81b0d9a697cc..000000000000 --- a/src/pages/EditReportFieldTextPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, {useCallback, useRef} from 'react'; -import {View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -type EditReportFieldTextPageProps = { - /** Value of the policy report field */ - fieldValue: string; - - /** Name of the policy report field */ - fieldName: string; - - /** Key of the policy report field */ - fieldKey: string; - - /** Flag to indicate if the field can be left blank */ - isRequired: boolean; - - /** Three dot menu item options */ - menuItems?: ThreeDotsMenuItem[]; - - /** Callback to fire when the Save button is pressed */ - onSubmit: (form: FormOnyxValues) => void; -}; - -function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldKey, menuItems}: EditReportFieldTextPageProps) { - const {windowWidth} = useWindowDimensions(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const inputRef = useRef(null); - - const validate = useCallback( - (values: FormOnyxValues) => { - const errors: FormInputErrors = {}; - if (isRequired && values[fieldKey].trim() === '') { - errors[fieldKey] = 'common.error.fieldRequired'; - } - return errors; - }, - [fieldKey, isRequired], - ); - - return ( - { - inputRef.current?.focus(); - }} - testID={EditReportFieldTextPage.displayName} - > - - - - - - - - ); -} - -EditReportFieldTextPage.displayName = 'EditReportFieldTextPage'; - -export default EditReportFieldTextPage; From b6f8d0bbca2f1f5fbb71344117b4cfc348ffb039 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 10:26:26 +0500 Subject: [PATCH 08/16] tidy up unleft comments --- src/libs/Permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 8b40ceafab65..1973e665b20f 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -15,7 +15,7 @@ function canUseDefaultRooms(betas: OnyxEntry): boolean { } function canUseReportFields(betas: OnyxEntry): boolean { - return true; // !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); + return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); } function canUseViolations(betas: OnyxEntry): boolean { From 253f3dbbc760baf443b49492ee231b6920f029cf Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 15:45:45 +0500 Subject: [PATCH 09/16] merge changes for option list utils --- src/libs/OptionsListUtils.ts | 285 ++++++++++++----------------------- 1 file changed, 100 insertions(+), 185 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 08c1e952bbec..1d8467a218e2 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -56,15 +56,6 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type SearchOption = ReportUtils.OptionData & { - item: T; -}; - -type OptionList = { - reports: Array>; - personalDetails: Array>; -}; - type Option = Partial; /** @@ -174,7 +165,7 @@ type GetOptions = { policyReportFieldOptions?: CategorySection[] | null; }; -type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -522,28 +513,6 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu : ''; } -/** - * Update alternate text for the option when applicable - */ -function getAlternateText( - option: ReportUtils.OptionData, - {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, -) { - if (!!option.isThread || !!option.isMoneyRequestReport) { - return lastMessageTextFromReport.length > 0 ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } - if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { - return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle; - } - if (option.isTaskReport) { - return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } - - return showChatPreviewLine && option.lastMessageText - ? option.lastMessageText - : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList[0].login ?? '' : ''); -} - /** * Get the last message text from the report directly or from other sources for special cases. */ @@ -581,7 +550,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); - const reportPreviewMessage = ReportUtils.getReportPreviewMessage( + lastMessageTextFromReport = ReportUtils.getReportPreviewMessage( !isEmptyObject(iouReport) ? iouReport : null, lastIOUMoneyReportAction, true, @@ -590,7 +559,6 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails true, lastReportAction, ); - lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(reportPreviewMessage); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { @@ -624,9 +592,8 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - config?: PreviewConfig, + {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, ): ReportUtils.OptionData { - const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, alternateText: null, @@ -659,7 +626,6 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, - lastMessageText: '', }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -668,8 +634,10 @@ function createOption( let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; + result.participantsList = personalDetailList; result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; + if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); result.isDefaultRoom = ReportUtils.isDefaultRoom(report); @@ -711,15 +679,16 @@ function createOption( lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; } - result.lastMessageText = lastMessageText; - - // If displaying chat preview line is needed, let's overwrite the default alternate text - result.alternateText = - showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); - - reportName = showPersonalDetails - ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') - : ReportUtils.getReportName(report); + if (result.isThread || result.isMoneyRequestReport) { + result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } else if (result.isChatRoom || result.isPolicyExpenseChat) { + result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; + } else if (result.isTaskReport) { + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } else { + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); + } + reportName = ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); @@ -862,7 +831,7 @@ function getSearchValueForPhoneOrEmail(searchTerm: string) { * Verifies that there is at least one enabled option */ function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { - return Object.values(options).some((option) => option.enabled && option.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + return Object.values(options).some((option) => option.enabled); } /** @@ -1430,63 +1399,12 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } -function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { - const reportMapForAccountIDs: Record = {}; - const allReportOptions: Array> = []; - - if (reports) { - Object.values(reports).forEach((report) => { - if (!report) { - return; - } - - const isSelfDM = ReportUtils.isSelfDM(report); - // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. - const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; - - if (!accountIDs || accountIDs.length === 0) { - return; - } - - // Save the report in the map if this is a single participant so we can associate the reportID with the - // personal detail option later. Individuals should not be associated with single participant - // policyExpenseChats or chatRooms since those are not people. - if (accountIDs.length <= 1) { - reportMapForAccountIDs[accountIDs[0]] = report; - } - - allReportOptions.push({ - item: report, - ...createOption(accountIDs, personalDetails, report, {}), - }); - }); - } - - const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ - item: personalDetail, - ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), - })); - - return { - reports: allReportOptions, - personalDetails: allPersonalDetailsOptions as Array>, - }; -} - -function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { - const accountIDs = report.participantAccountIDs ?? []; - - return { - item: report, - ...createOption(accountIDs, personalDetails, report, {}), - }; -} - /** - * filter options based on specific conditions + * Build the options */ function getOptions( - options: OptionList, + reports: OnyxCollection, + personalDetails: OnyxEntry, { reportActions = {}, betas = [], @@ -1567,7 +1485,9 @@ function getOptions( }; } + if (includePolicyReportFieldOptions) { + const transformedPolicyReportFieldOptions = getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue); return { recentReports: [], personalDetails: [], @@ -1576,7 +1496,7 @@ function getOptions( categoryOptions: [], tagOptions: [], taxRatesOptions: [], - policyReportFieldOptions: getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue), + policyReportFieldOptions: transformedPolicyReportFieldOptions, }; } @@ -1597,12 +1517,9 @@ function getOptions( const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); - const topmostReportId = Navigation.getTopmostReportId() ?? ''; // Filter out all the reports that shouldn't be displayed - const filteredReportOptions = options.reports.filter((option) => { - const report = option.item; - + const filteredReports = Object.values(reports ?? {}).filter((report) => { const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1611,7 +1528,7 @@ function getOptions( return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: topmostReportId, + currentReportId: Navigation.getTopmostReportId() ?? '', betas, policies, doesReportHaveViolations, @@ -1624,28 +1541,27 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { - const report = option.item; - if (option.isArchivedRoom) { + const orderedReports = lodashSortBy(filteredReports, (report) => { + if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } return report?.lastVisibleActionCreated; }); - orderedReportOptions.reverse(); - - const allReportOptions = orderedReportOptions.filter((option) => { - const report = option.item; + orderedReports.reverse(); + const allReportOptions: ReportUtils.OptionData[] = []; + orderedReports.forEach((report) => { if (!report) { return; } - const isThread = option.isThread; - const isTaskReport = option.isTaskReport; - const isPolicyExpenseChat = option.isPolicyExpenseChat; - const isMoneyRequestReport = option.isMoneyRequestReport; - const isSelfDM = option.isSelfDM; + const isThread = ReportUtils.isChatThread(report); + const isChatRoom = ReportUtils.isChatRoom(report); + const isTaskReport = ReportUtils.isTaskReport(report); + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); + const isSelfDM = ReportUtils.isSelfDM(report); // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; @@ -1683,11 +1599,33 @@ function getOptions( return; } - return option; - }); + // Save the report in the map if this is a single participant so we can associate the reportID with the + // personal detail option later. Individuals should not be associated with single participant + // policyExpenseChats or chatRooms since those are not people. + if (accountIDs.length <= 1 && !isPolicyExpenseChat && !isChatRoom) { + reportMapForAccountIDs[accountIDs[0]] = report; + } - const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); - let allPersonalDetailsOptions = havingLoginPersonalDetails; + allReportOptions.push( + createOption(accountIDs, personalDetails, report, reportActions, { + showChatPreviewLine, + forcePolicyNamePreview, + }), + ); + }); + // We're only picking personal details that have logins set + // This is a temporary fix for all the logic that's been breaking because of the new privacy changes + // See https://github.com/Expensify/Expensify/issues/293465 for more context + // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText + const havingLoginPersonalDetails = !includeP2P + ? {} + : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); + let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => + createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { + showChatPreviewLine, + forcePolicyNamePreview, + }), + ); if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 @@ -1708,17 +1646,8 @@ function getOptions( optionsToExclude.push({login}); }); - let recentReportOptions = []; - let personalDetailsOptions: ReportUtils.OptionData[] = []; - if (includeRecentReports) { for (const reportOption of allReportOptions) { - /** - * By default, generated options does not have the chat preview line enabled. - * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. - */ - reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); - // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1806,7 +1735,7 @@ function getOptions( !isCurrentUser({login: searchValue} as PersonalDetails) && selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers @@ -1814,7 +1743,7 @@ function getOptions( // Generates an optimistic account ID for new users not yet saved in Onyx const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { - ...allPersonalDetails, + ...personalDetails, [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, @@ -1882,10 +1811,10 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const optionList = getOptions(options, { + const options = getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1904,11 +1833,11 @@ function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return optionList; + return options; } -function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { - return getOptions(options, { +function getShareLogOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { + return getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1959,8 +1888,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Array> = [], - personalDetails: Array> = [], + reports: OnyxCollection, + personalDetails: OnyxEntry, betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1978,9 +1907,6 @@ function getFilteredOptions( includeTaxRates = false, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, - includePolicyReportFieldOptions = false, - policyReportFieldOptions: string[] = [], - recentlyUsedPolicyReportFieldOptions: string[] = [], ) { return getOptions(reports, personalDetails, { betas, @@ -2003,9 +1929,6 @@ function getFilteredOptions( includeTaxRates, taxRates, includeSelfDM, - includePolicyReportFieldOptions, - policyReportFieldOptions, - recentlyUsedPolicyReportFieldOptions, }); } @@ -2014,8 +1937,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Array> = [], - personalDetails: Array> = [], + reports: Record, + personalDetails: OnyxEntry, betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -2023,27 +1946,24 @@ function getShareDestinationOptions( includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { - return getOptions( - {reports, personalDetails}, - { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - maxRecentReportsToShow: 0, // Unlimited - includeRecentReports: true, - includeMultipleParticipantReports: true, - includePersonalDetails: false, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - excludeUnknownUsers, - includeSelfDM: true, - }, - ); + return getOptions(reports, personalDetails, { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + maxRecentReportsToShow: 0, // Unlimited + includeRecentReports: true, + includeMultipleParticipantReports: true, + includePersonalDetails: false, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + excludeUnknownUsers, + includeSelfDM: true, + }); } /** @@ -2076,23 +1996,20 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { * Build the options for the Workspace Member Invite view */ function getMemberInviteOptions( - personalDetails: Array>, + personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false, ): GetOptions { - return getOptions( - {reports: [], personalDetails}, - { - betas, - searchInputValue: searchValue.trim(), - includePersonalDetails: true, - excludeLogins, - sortPersonalDetailsByAlphaAsc: true, - includeSelectedOptions, - }, - ); + return getOptions({}, personalDetails, { + betas, + searchInputValue: searchValue.trim(), + includePersonalDetails: true, + excludeLogins, + sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, + }); } /** @@ -2232,10 +2149,8 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, - createOptionList, - createOptionFromReport, getReportOption, getTaxRatesSection, }; -export type {MemberForList, CategorySection, GetOptions, OptionList, SearchOption, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; From b478cdf34b6fc259d07246f874975034aa68ea95 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 15:53:32 +0500 Subject: [PATCH 10/16] undo merge conflict changes --- src/components/OptionListContextProvider.tsx | 142 ------------------ src/pages/NewChatPage.tsx | 36 +++-- src/pages/RoomInvitePage.tsx | 37 +++-- src/pages/SearchPage/index.tsx | 42 +++--- ...yForRefactorRequestParticipantsSelector.js | 38 ++--- .../MoneyRequestParticipantsSelector.js | 28 +++- .../ShareLogList/BaseShareLogList.tsx | 23 +-- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 27 ++-- .../TaskShareDestinationSelectorModal.tsx | 83 +++++----- src/pages/workspace/WorkspaceInvitePage.tsx | 15 +- 10 files changed, 180 insertions(+), 291 deletions(-) delete mode 100644 src/components/OptionListContextProvider.tsx diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx deleted file mode 100644 index 43c5906d4900..000000000000 --- a/src/components/OptionListContextProvider.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import type {OptionList} from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; -import {usePersonalDetails} from './OnyxProvider'; - -type OptionsListContextProps = { - /** List of options for reports and personal details */ - options: OptionList; - /** Function to initialize the options */ - initializeOptions: () => void; - /** Flag to check if the options are initialized */ - areOptionsInitialized: boolean; -}; - -type OptionsListProviderOnyxProps = { - /** Collection of reports */ - reports: OnyxCollection; -}; - -type OptionsListProviderProps = OptionsListProviderOnyxProps & { - /** Actual content wrapped by this component */ - children: React.ReactNode; -}; - -const OptionsListContext = createContext({ - options: { - reports: [], - personalDetails: [], - }, - initializeOptions: () => {}, - areOptionsInitialized: false, -}); - -function OptionsListContextProvider({reports, children}: OptionsListProviderProps) { - const areOptionsInitialized = useRef(false); - const [options, setOptions] = useState({ - reports: [], - personalDetails: [], - }); - const personalDetails = usePersonalDetails(); - - useEffect(() => { - // there is no need to update the options if the options are not initialized - if (!areOptionsInitialized.current) { - return; - } - - const lastUpdatedReport = ReportUtils.getLastUpdatedReport(); - - if (!lastUpdatedReport) { - return; - } - - const newOption = OptionsListUtils.createOptionFromReport(lastUpdatedReport, personalDetails); - const replaceIndex = options.reports.findIndex((option) => option.reportID === lastUpdatedReport.reportID); - - if (replaceIndex === -1) { - return; - } - - setOptions((prevOptions) => { - const newOptions = {...prevOptions}; - newOptions.reports[replaceIndex] = newOption; - return newOptions; - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reports]); - - useEffect(() => { - // there is no need to update the options if the options are not initialized - if (!areOptionsInitialized.current) { - return; - } - - // since personal details are not a collection, we need to recreate the whole list from scratch - const newPersonalDetailsOptions = OptionsListUtils.createOptionList(personalDetails).personalDetails; - - setOptions((prevOptions) => { - const newOptions = {...prevOptions}; - newOptions.personalDetails = newPersonalDetailsOptions; - return newOptions; - }); - }, [personalDetails]); - - const loadOptions = useCallback(() => { - const optionLists = OptionsListUtils.createOptionList(personalDetails, reports); - setOptions({ - reports: optionLists.reports, - personalDetails: optionLists.personalDetails, - }); - }, [personalDetails, reports]); - - const initializeOptions = useCallback(() => { - if (areOptionsInitialized.current) { - return; - } - - loadOptions(); - areOptionsInitialized.current = true; - }, [loadOptions]); - - return ( - ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current}), [options, initializeOptions])}> - {children} - - ); -} - -const useOptionsListContext = () => useContext(OptionsListContext); - -// Hook to use the OptionsListContext with an initializer to load the options -const useOptionsList = (options?: {shouldInitialize: boolean}) => { - const {shouldInitialize = true} = options ?? {}; - const {initializeOptions, options: optionsList, areOptionsInitialized} = useOptionsListContext(); - - useEffect(() => { - if (!shouldInitialize || areOptionsInitialized) { - return; - } - - initializeOptions(); - }, [shouldInitialize, initializeOptions, areOptionsInitialized]); - - return { - initializeOptions, - options: optionsList, - areOptionsInitialized, - }; -}; - -export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, -})(OptionsListContextProvider); - -export {useOptionsListContext, useOptionsList, OptionsListContext}; diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 751813d1d3cf..c1c4717a295b 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,10 +1,9 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; -import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -19,6 +18,7 @@ import doInteractionTask from '@libs/DoInteractionTask'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; @@ -29,6 +29,9 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; type NewChatPageWithOnyxProps = { + /** All reports shared with the user */ + reports: OnyxCollection; + /** New group chat draft data */ newGroupDraft: OnyxEntry; @@ -50,9 +53,8 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) { +function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const personalData = useCurrentUserPersonalDetails(); @@ -70,16 +72,13 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports }; const [searchTerm, setSearchTerm] = useState(''); - const [filteredRecentReports, setFilteredRecentReports] = useState([]); - const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); - const [filteredUserToInvite, setFilteredUserToInvite] = useState(); + const [filteredRecentReports, setFilteredRecentReports] = useState([]); + const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); + const [filteredUserToInvite, setFilteredUserToInvite] = useState(); const [selectedOptions, setSelectedOptions] = useState(getGroupParticipants); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -92,6 +91,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())), ); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); + const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; @@ -144,8 +145,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - options.reports ?? [], - options.personalDetails ?? [], + reports, + personalDetails, betas ?? [], searchTerm, newSelectedOptions, @@ -205,8 +206,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - options.reports ?? [], - options.personalDetails ?? [], + reports, + personalDetails, betas ?? [], searchTerm, selectedOptions, @@ -227,7 +228,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports setFilteredUserToInvite(userToInvite); // props.betas is not added as dependency since it doesn't change during the component lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [options, searchTerm]); + }, [reports, personalDetails, searchTerm]); useEffect(() => { const interactionTask = doInteractionTask(() => { @@ -289,7 +290,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports headerMessage={headerMessage} boldStyle shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - shouldShowOptions={areOptionsInitialized} + shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd} shouldShowConfirmButton shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} @@ -318,6 +319,9 @@ export default withOnyx({ newGroupDraft: { key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT, }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 49e53381e040..77b5c48d8a72 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -2,10 +2,11 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; @@ -24,25 +25,30 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; import SearchInputManager from './workspace/SearchInputManager'; -type RoomInvitePageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps; +type RoomInvitePageOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; +}; + +type RoomInvitePageProps = RoomInvitePageOnyxProps & WithReportOrNotFoundProps & WithNavigationTransitionEndProps; type Sections = Array>>; -function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { +function RoomInvitePage({betas, personalDetails, report, policies, didScreenTransitionEnd}: RoomInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); - const {options, areOptionsInitialized} = useOptionsList(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -58,7 +64,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetails, betas ?? [], searchTerm, excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; @@ -77,12 +83,12 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { setInvitePersonalDetails(inviteOptions.personalDetails); setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [betas, searchTerm, excludedUsers, options.personalDetails]); + }, [personalDetails, betas, searchTerm, excludedUsers]); const sections = useMemo(() => { const sectionsArr: Sections = []; - if (!areOptionsInitialized) { + if (!didScreenTransitionEnd) { return []; } @@ -124,7 +130,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { } return sectionsArr; - }, [areOptionsInitialized, selectedOptions, searchTerm, invitePersonalDetails, userToInvite, translate]); + }, [invitePersonalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -187,7 +193,6 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, Boolean(userToInvite), searchValue); }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); - return ( ({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + })(RoomInvitePage), + ), +); diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index c072bfd56913..b1555fd1cab8 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -1,10 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; +import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -17,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import type {OptionData} from '@libs/ReportUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -30,6 +30,9 @@ type SearchPageOnyxProps = { /** Beta features list */ betas: OnyxEntry; + /** All reports shared with the user */ + reports: OnyxCollection; + /** Whether or not we are searching for reports on the server */ isSearchingForReports: OnyxEntry; }; @@ -37,7 +40,7 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; type SearchPageSectionItem = { - data: OptionData[]; + data: ReportUtils.OptionData[]; shouldShow: boolean; }; @@ -50,14 +53,12 @@ const setPerformanceTimersEnd = () => { const SearchPageFooterInstance = ; -function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) { +function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); + const personalDetails = usePersonalDetails(); const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -78,7 +79,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) userToInvite, headerMessage, } = useMemo(() => { - if (!areOptionsInitialized) { + if (!isScreenTransitionEnd) { return { recentReports: [], personalDetails: [], @@ -86,10 +87,10 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) headerMessage: '', }; } - const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); - const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); - return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, options, debouncedSearchValue, betas]); + const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas ?? []); + const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + return {...options, headerMessage: header}; + }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); const sections = useMemo((): SearchPageSectionList => { const newSections: SearchPageSectionList = []; @@ -118,7 +119,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) return newSections; }, [localPersonalDetails, recentReports, userToInvite]); - const selectReport = (option: OptionData) => { + const selectReport = (option: ReportUtils.OptionData) => { if (!option) { return; } @@ -135,6 +136,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) setIsScreenTransitionEnd(true); }; + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( - {({safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( <> - - sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} + + sections={didScreenTransitionEnd && isOptionsDataReady ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -161,7 +164,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined} onLayout={setPerformanceTimersEnd} onSelectRow={selectReport} - showLoadingPlaceholder={!areOptionsInitialized} + showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> @@ -175,6 +178,9 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) SearchPage.displayName = 'SearchPage'; export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, betas: { key: ONYXKEYS.BETAS, }, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index a05167d5cedf..4870d39002ac 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -7,7 +7,6 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -20,6 +19,8 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -48,6 +49,9 @@ const propTypes = { }), ), + /** All reports shared with the user */ + reports: PropTypes.objectOf(reportPropTypes), + /** Padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -64,6 +68,7 @@ const propTypes = { const defaultProps = { participants: [], safeAreaPaddingBottomStyle: {}, + reports: {}, betas: [], dismissedReferralBanners: {}, didScreenTransitionEnd: false, @@ -72,6 +77,7 @@ const defaultProps = { function MoneyTemporaryForRefactorRequestParticipantsSelector({ betas, participants, + reports, onFinish, onParticipantsAdded, safeAreaPaddingBottomStyle, @@ -87,9 +93,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -106,12 +109,12 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; - if (!areOptionsInitialized) { + if (!didScreenTransitionEnd) { return [newSections, {}]; } const chatOptions = OptionsListUtils.getFilteredOptions( - options.reports, - options.personalDetails, + reports, + personalDetails, betas, debouncedSearchTerm, participants, @@ -172,20 +175,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [ - areOptionsInitialized, - options.reports, - options.personalDetails, - betas, - debouncedSearchTerm, - participants, - iouType, - canUseP2PDistanceRequests, - iouRequestType, - maxParticipantsReached, - personalDetails, - translate, - ]); + }, [didScreenTransitionEnd, reports, personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, iouRequestType, maxParticipantsReached, translate]); /** * Adds a single participant to the request @@ -352,11 +342,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( 0 ? safeAreaPaddingBottomStyle : {}]}> diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 05ef5baa8432..16608ba13de8 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -7,7 +7,6 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -20,6 +19,7 @@ import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,6 +50,9 @@ const propTypes = { }), ), + /** All reports shared with the user */ + reports: PropTypes.objectOf(reportPropTypes), + /** padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -58,26 +61,33 @@ const propTypes = { /** Whether the money request is a distance request or not */ isDistanceRequest: PropTypes.bool, + + /** Whether we are searching for reports in the server */ + isSearchingForReports: PropTypes.bool, }; const defaultProps = { dismissedReferralBanners: {}, participants: [], safeAreaPaddingBottomStyle: {}, + reports: {}, betas: [], isDistanceRequest: false, + isSearchingForReports: false, }; function MoneyRequestParticipantsSelector({ betas, dismissedReferralBanners, participants, + reports, navigateToRequest, navigateToSplit, onAddParticipants, safeAreaPaddingBottomStyle, iouType, isDistanceRequest, + isSearchingForReports, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -86,7 +96,6 @@ function MoneyRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); - const {options, areOptionsInitialized} = useOptionsList(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -95,8 +104,8 @@ function MoneyRequestParticipantsSelector({ const newChatOptions = useMemo(() => { const chatOptions = OptionsListUtils.getFilteredOptions( - options.reports, - options.personalDetails, + reports, + personalDetails, betas, searchTerm, participants, @@ -123,7 +132,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); + }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]); /** * Returns the sections needed for the OptionsSelector @@ -356,7 +365,7 @@ function MoneyRequestParticipantsSelector({ onSelectRow={addSingleParticipant} footerContent={footerContent} headerMessage={headerMessage} - showLoadingPlaceholder={!areOptionsInitialized} + showLoadingPlaceholder={isSearchingForReports} rightHandSideComponent={itemRightSideComponent} /> @@ -371,7 +380,14 @@ export default withOnyx({ dismissedReferralBanners: { key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, betas: { key: ONYXKEYS.BETAS, }, + isSearchingForReports: { + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + initWithStoredValues: false, + }, })(MoneyRequestParticipantsSelector); diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index cee62380a011..70c2d301b9ac 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; +import {usePersonalDetails} from '@components/OnyxProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -11,45 +11,46 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {BaseShareLogListOnyxProps, BaseShareLogListProps} from './types'; -function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { +function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogListProps) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState>({ recentReports: [], personalDetails: [], userToInvite: null, }); + const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); const isMounted = useRef(false); - const {options, areOptionsInitialized} = useOptionsList(); + const personalDetails = usePersonalDetails(); + const updateOptions = useCallback(() => { const { recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, - } = OptionsListUtils.getShareLogOptions(options, searchValue.trim(), betas ?? []); + } = OptionsListUtils.getShareLogOptions(reports, personalDetails, searchValue.trim(), betas ?? []); setSearchOptions({ recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, }); - }, [betas, options, searchValue]); + }, [betas, personalDetails, reports, searchValue]); - useEffect(() => { - if (!areOptionsInitialized) { - return; - } + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); + useEffect(() => { updateOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [options, areOptionsInitialized]); + }, []); useEffect(() => { if (!isMounted.current) { @@ -125,7 +126,7 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { value={searchValue} headerMessage={headerMessage} showTitleTooltip - shouldShowOptions={areOptionsInitialized} + shouldShowOptions={isOptionsDataReady} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 7a6ff74087de..bb199ddc905f 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -7,8 +7,7 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useBetas, useSession} from '@components/OnyxProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; +import {useBetas, usePersonalDetails, useSession} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; @@ -40,18 +39,22 @@ type TaskAssigneeSelectorModalOnyxProps = { task: OnyxEntry; }; +type UseOptions = { + reports: OnyxCollection; +}; + type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps & WithNavigationTransitionEndProps; -function useOptions() { +function useOptions({reports}: UseOptions) { + const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const {options: optionsList, areOptionsInitialized} = useOptionsList(); const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( - optionsList.reports, - optionsList.personalDetails, + reports, + allPersonalDetails, betas, debouncedSearchValue.trim(), [], @@ -84,18 +87,18 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue, isLoading]); + }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]); - return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; + return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; } -function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { +function TaskAssigneeSelectorModal({reports, task, didScreenTransitionEnd}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); @@ -212,14 +215,14 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro /> diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index b4b8f9084a57..5b56e58752ac 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,9 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; +import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -22,6 +22,8 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; type TaskShareDestinationSelectorModalOnyxProps = { + reports: OnyxCollection; + isSearchingForReports: OnyxEntry; }; @@ -38,36 +40,29 @@ const selectReportHandler = (option: unknown) => { Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reportOptions: Array>) => - (reportOptions ?? []).reduce((filtered: Array>, option) => { - const report = option.item; +const reportFilter = (reports: OnyxCollection) => + Object.keys(reports ?? {}).reduce((filtered, reportKey) => { + const report: OnyxEntry = reports?.[reportKey] ?? null; if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - filtered.push(option); + return {...filtered, [reportKey]: report}; } return filtered; - }, []); + }, {}); -function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); +function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); + const personalDetails = usePersonalDetails(); const {isOffline} = useNetwork(); - const {options: optionList, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); const options = useMemo(() => { - if (!areOptionsInitialized) { - return { - sections: [], - headerMessage: '', - }; - } - const filteredReports = reportFilter(optionList.reports); - const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, optionList.personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); + const filteredReports = reportFilter(reports); + + const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); + const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); const sections = @@ -89,7 +84,7 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes : []; return {sections, headerMessage}; - }, [areOptionsInitialized, optionList.reports, optionList.personalDetails, debouncedSearchValue]); + }, [personalDetails, reports, debouncedSearchValue]); useEffect(() => { ReportActions.searchInServer(debouncedSearchValue); @@ -99,28 +94,29 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes setDidScreenTransitionEnd(true)} > - <> - Navigation.goBack(ROUTES.NEW_TASK)} - /> - - ( + <> + Navigation.goBack(ROUTES.NEW_TASK)} /> - - + + + + + )} ); } @@ -128,6 +124,9 @@ function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDes TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, isSearchingForReports: { key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3f95c3e02a5b..014097cd019c 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -7,7 +7,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; @@ -76,9 +75,6 @@ function WorkspaceInvitePage({ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -102,7 +98,8 @@ function WorkspaceInvitePage({ const newPersonalDetailsDict: Record = {}; const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); + // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { @@ -153,12 +150,12 @@ function WorkspaceInvitePage({ setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [options.personalDetails, policyMembers, betas, searchTerm, excludedUsers]); + }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; - if (!areOptionsInitialized) { + if (!didScreenTransitionEnd) { return []; } @@ -206,7 +203,7 @@ function WorkspaceInvitePage({ }); return sectionsArr; - }, [areOptionsInitialized, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); + }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -307,7 +304,7 @@ function WorkspaceInvitePage({ onSelectRow={toggleOption} onConfirm={inviteUser} showScrollIndicator - showLoadingPlaceholder={!areOptionsInitialized} + showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} /> From e35fe68d43f2c27f801b22718dcbfaac2426c2ec Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 16:02:05 +0500 Subject: [PATCH 11/16] fix broken merge --- src/App.tsx | 2 -- src/libs/OptionsListUtils.ts | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 61874dc72fb0..a3a9f7a3f3b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,6 @@ import HTMLEngineProvider from './components/HTMLEngineProvider'; import InitialURLContextProvider from './components/InitialURLContextProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; -import OptionsListContextProvider from './components/OptionListContextProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; @@ -83,7 +82,6 @@ function App({url}: AppProps) { FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, - OptionsListContextProvider, ]} > diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1d8467a218e2..9f17c9b8192e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1485,7 +1485,6 @@ function getOptions( }; } - if (includePolicyReportFieldOptions) { const transformedPolicyReportFieldOptions = getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue); return { @@ -1907,6 +1906,9 @@ function getFilteredOptions( includeTaxRates = false, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions: string[] = [], + recentlyUsedPolicyReportFieldOptions: string[] = [], ) { return getOptions(reports, personalDetails, { betas, @@ -1929,6 +1931,9 @@ function getFilteredOptions( includeTaxRates, taxRates, includeSelfDM, + includePolicyReportFieldOptions, + policyReportFieldOptions, + recentlyUsedPolicyReportFieldOptions, }); } From 1f0f3dc87d803d9fad82dd46bc7c11ad1a4edf59 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 16:17:22 +0500 Subject: [PATCH 12/16] fix broken merge again --- src/App.tsx | 2 + src/components/OptionListContextProvider.tsx | 142 ++++++++ src/libs/OptionsListUtils.ts | 343 +++++++++++------- src/libs/Permissions.ts | 2 +- src/pages/NewChatPage.tsx | 36 +- src/pages/RoomInvitePage.tsx | 37 +- src/pages/SearchPage/index.tsx | 42 +-- ...yForRefactorRequestParticipantsSelector.js | 38 +- .../MoneyRequestParticipantsSelector.js | 28 +- .../ShareLogList/BaseShareLogList.tsx | 23 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 27 +- .../TaskShareDestinationSelectorModal.tsx | 83 ++--- src/pages/workspace/WorkspaceInvitePage.tsx | 15 +- 13 files changed, 500 insertions(+), 318 deletions(-) create mode 100644 src/components/OptionListContextProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index a3a9f7a3f3b6..61874dc72fb0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import HTMLEngineProvider from './components/HTMLEngineProvider'; import InitialURLContextProvider from './components/InitialURLContextProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; +import OptionsListContextProvider from './components/OptionListContextProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; @@ -82,6 +83,7 @@ function App({url}: AppProps) { FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, + OptionsListContextProvider, ]} > diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx new file mode 100644 index 000000000000..43c5906d4900 --- /dev/null +++ b/src/components/OptionListContextProvider.tsx @@ -0,0 +1,142 @@ +import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {OptionList} from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import {usePersonalDetails} from './OnyxProvider'; + +type OptionsListContextProps = { + /** List of options for reports and personal details */ + options: OptionList; + /** Function to initialize the options */ + initializeOptions: () => void; + /** Flag to check if the options are initialized */ + areOptionsInitialized: boolean; +}; + +type OptionsListProviderOnyxProps = { + /** Collection of reports */ + reports: OnyxCollection; +}; + +type OptionsListProviderProps = OptionsListProviderOnyxProps & { + /** Actual content wrapped by this component */ + children: React.ReactNode; +}; + +const OptionsListContext = createContext({ + options: { + reports: [], + personalDetails: [], + }, + initializeOptions: () => {}, + areOptionsInitialized: false, +}); + +function OptionsListContextProvider({reports, children}: OptionsListProviderProps) { + const areOptionsInitialized = useRef(false); + const [options, setOptions] = useState({ + reports: [], + personalDetails: [], + }); + const personalDetails = usePersonalDetails(); + + useEffect(() => { + // there is no need to update the options if the options are not initialized + if (!areOptionsInitialized.current) { + return; + } + + const lastUpdatedReport = ReportUtils.getLastUpdatedReport(); + + if (!lastUpdatedReport) { + return; + } + + const newOption = OptionsListUtils.createOptionFromReport(lastUpdatedReport, personalDetails); + const replaceIndex = options.reports.findIndex((option) => option.reportID === lastUpdatedReport.reportID); + + if (replaceIndex === -1) { + return; + } + + setOptions((prevOptions) => { + const newOptions = {...prevOptions}; + newOptions.reports[replaceIndex] = newOption; + return newOptions; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reports]); + + useEffect(() => { + // there is no need to update the options if the options are not initialized + if (!areOptionsInitialized.current) { + return; + } + + // since personal details are not a collection, we need to recreate the whole list from scratch + const newPersonalDetailsOptions = OptionsListUtils.createOptionList(personalDetails).personalDetails; + + setOptions((prevOptions) => { + const newOptions = {...prevOptions}; + newOptions.personalDetails = newPersonalDetailsOptions; + return newOptions; + }); + }, [personalDetails]); + + const loadOptions = useCallback(() => { + const optionLists = OptionsListUtils.createOptionList(personalDetails, reports); + setOptions({ + reports: optionLists.reports, + personalDetails: optionLists.personalDetails, + }); + }, [personalDetails, reports]); + + const initializeOptions = useCallback(() => { + if (areOptionsInitialized.current) { + return; + } + + loadOptions(); + areOptionsInitialized.current = true; + }, [loadOptions]); + + return ( + ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current}), [options, initializeOptions])}> + {children} + + ); +} + +const useOptionsListContext = () => useContext(OptionsListContext); + +// Hook to use the OptionsListContext with an initializer to load the options +const useOptionsList = (options?: {shouldInitialize: boolean}) => { + const {shouldInitialize = true} = options ?? {}; + const {initializeOptions, options: optionsList, areOptionsInitialized} = useOptionsListContext(); + + useEffect(() => { + if (!shouldInitialize || areOptionsInitialized) { + return; + } + + initializeOptions(); + }, [shouldInitialize, initializeOptions, areOptionsInitialized]); + + return { + initializeOptions, + options: optionsList, + areOptionsInitialized, + }; +}; + +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, +})(OptionsListContextProvider); + +export {useOptionsListContext, useOptionsList, OptionsListContext}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9f17c9b8192e..38251cb1fae9 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -56,6 +56,15 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +type SearchOption = ReportUtils.OptionData & { + item: T; +}; + +type OptionList = { + reports: Array>; + personalDetails: Array>; +}; + type Option = Partial; /** @@ -165,7 +174,7 @@ type GetOptions = { policyReportFieldOptions?: CategorySection[] | null; }; -type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -513,6 +522,28 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu : ''; } +/** + * Update alternate text for the option when applicable + */ +function getAlternateText( + option: ReportUtils.OptionData, + {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, +) { + if (!!option.isThread || !!option.isMoneyRequestReport) { + return lastMessageTextFromReport.length > 0 ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { + return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle; + } + if (option.isTaskReport) { + return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + + return showChatPreviewLine && option.lastMessageText + ? option.lastMessageText + : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList[0].login ?? '' : ''); +} + /** * Get the last message text from the report directly or from other sources for special cases. */ @@ -550,7 +581,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); - lastMessageTextFromReport = ReportUtils.getReportPreviewMessage( + const reportPreviewMessage = ReportUtils.getReportPreviewMessage( !isEmptyObject(iouReport) ? iouReport : null, lastIOUMoneyReportAction, true, @@ -559,6 +590,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails true, lastReportAction, ); + lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(reportPreviewMessage); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { @@ -592,8 +624,9 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, + config?: PreviewConfig, ): ReportUtils.OptionData { + const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, alternateText: null, @@ -626,6 +659,7 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, + lastMessageText: '', }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -634,10 +668,8 @@ function createOption( let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; - result.participantsList = personalDetailList; result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; - if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); result.isDefaultRoom = ReportUtils.isDefaultRoom(report); @@ -679,16 +711,15 @@ function createOption( lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; } - if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else if (result.isChatRoom || result.isPolicyExpenseChat) { - result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; - } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); - } - reportName = ReportUtils.getReportName(report); + result.lastMessageText = lastMessageText; + + // If displaying chat preview line is needed, let's overwrite the default alternate text + result.alternateText = + showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); + + reportName = showPersonalDetails + ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') + : ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); @@ -831,7 +862,7 @@ function getSearchValueForPhoneOrEmail(searchTerm: string) { * Verifies that there is at least one enabled option */ function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { - return Object.values(options).some((option) => option.enabled); + return Object.values(options).some((option) => option.enabled && option.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** @@ -1399,12 +1430,63 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { + const reportMapForAccountIDs: Record = {}; + const allReportOptions: Array> = []; + + if (reports) { + Object.values(reports).forEach((report) => { + if (!report) { + return; + } + + const isSelfDM = ReportUtils.isSelfDM(report); + // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. + const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; + + if (!accountIDs || accountIDs.length === 0) { + return; + } + + // Save the report in the map if this is a single participant so we can associate the reportID with the + // personal detail option later. Individuals should not be associated with single participant + // policyExpenseChats or chatRooms since those are not people. + if (accountIDs.length <= 1) { + reportMapForAccountIDs[accountIDs[0]] = report; + } + + allReportOptions.push({ + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }); + }); + } + + const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ + item: personalDetail, + ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), + })); + + return { + reports: allReportOptions, + personalDetails: allPersonalDetailsOptions as Array>, + }; +} + +function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { + const accountIDs = report.participantAccountIDs ?? []; + + return { + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }; +} + /** - * Build the options + * filter options based on specific conditions */ function getOptions( - reports: OnyxCollection, - personalDetails: OnyxEntry, + options: OptionList, { reportActions = {}, betas = [], @@ -1499,26 +1581,14 @@ function getOptions( }; } - if (!isPersonalDetailsReady(personalDetails)) { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], - }; - } - - let recentReportOptions = []; - let personalDetailsOptions: ReportUtils.OptionData[] = []; - const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const topmostReportId = Navigation.getTopmostReportId() ?? ''; // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports ?? {}).filter((report) => { + const filteredReportOptions = options.reports.filter((option) => { + const report = option.item; + const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1527,7 +1597,7 @@ function getOptions( return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: Navigation.getTopmostReportId() ?? '', + currentReportId: topmostReportId, betas, policies, doesReportHaveViolations, @@ -1540,27 +1610,28 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(filteredReports, (report) => { - if (ReportUtils.isArchivedRoom(report)) { + const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { + const report = option.item; + if (option.isArchivedRoom) { return CONST.DATE.UNIX_EPOCH; } return report?.lastVisibleActionCreated; }); - orderedReports.reverse(); + orderedReportOptions.reverse(); + + const allReportOptions = orderedReportOptions.filter((option) => { + const report = option.item; - const allReportOptions: ReportUtils.OptionData[] = []; - orderedReports.forEach((report) => { if (!report) { return; } - const isThread = ReportUtils.isChatThread(report); - const isChatRoom = ReportUtils.isChatRoom(report); - const isTaskReport = ReportUtils.isTaskReport(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const isSelfDM = ReportUtils.isSelfDM(report); + const isThread = option.isThread; + const isTaskReport = option.isTaskReport; + const isPolicyExpenseChat = option.isPolicyExpenseChat; + const isMoneyRequestReport = option.isMoneyRequestReport; + const isSelfDM = option.isSelfDM; // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; @@ -1598,33 +1669,11 @@ function getOptions( return; } - // Save the report in the map if this is a single participant so we can associate the reportID with the - // personal detail option later. Individuals should not be associated with single participant - // policyExpenseChats or chatRooms since those are not people. - if (accountIDs.length <= 1 && !isPolicyExpenseChat && !isChatRoom) { - reportMapForAccountIDs[accountIDs[0]] = report; - } - - allReportOptions.push( - createOption(accountIDs, personalDetails, report, reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + return option; }); - // We're only picking personal details that have logins set - // This is a temporary fix for all the logic that's been breaking because of the new privacy changes - // See https://github.com/Expensify/Expensify/issues/293465 for more context - // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText - const havingLoginPersonalDetails = !includeP2P - ? {} - : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); - let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + + const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); + let allPersonalDetailsOptions = havingLoginPersonalDetails; if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 @@ -1645,8 +1694,17 @@ function getOptions( optionsToExclude.push({login}); }); + let recentReportOptions = []; + let personalDetailsOptions: ReportUtils.OptionData[] = []; + if (includeRecentReports) { for (const reportOption of allReportOptions) { + /** + * By default, generated options does not have the chat preview line enabled. + * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. + */ + reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); + // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1734,7 +1792,7 @@ function getOptions( !isCurrentUser({login: searchValue} as PersonalDetails) && selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers @@ -1742,7 +1800,7 @@ function getOptions( // Generates an optimistic account ID for new users not yet saved in Onyx const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { - ...personalDetails, + ...allPersonalDetails, [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, @@ -1810,10 +1868,10 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const options = getOptions(reports, personalDetails, { + const optionList = getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1832,11 +1890,11 @@ function getSearchOptions(reports: OnyxCollection, personalDetails: Onyx Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return options; + return optionList; } -function getShareLogOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { - return getOptions(reports, personalDetails, { +function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions { + return getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1887,8 +1945,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Array, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1910,31 +1968,34 @@ function getFilteredOptions( policyReportFieldOptions: string[] = [], recentlyUsedPolicyReportFieldOptions: string[] = [], ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - includeRecentReports: true, - includePersonalDetails: true, - maxRecentReportsToShow: 5, - excludeLogins, - includeOwnedWorkspaceChats, - includeP2P, - includeCategories, - categories, - recentlyUsedCategories, - includeTags, - tags, - recentlyUsedTags, - canInviteUser, - includeSelectedOptions, - includeTaxRates, - taxRates, - includeSelfDM, - includePolicyReportFieldOptions, - policyReportFieldOptions, - recentlyUsedPolicyReportFieldOptions, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + includeRecentReports: true, + includePersonalDetails: true, + maxRecentReportsToShow: 5, + excludeLogins, + includeOwnedWorkspaceChats, + includeP2P, + includeCategories, + categories, + recentlyUsedCategories, + includeTags, + tags, + recentlyUsedTags, + canInviteUser, + includeSelectedOptions, + includeTaxRates, + taxRates, + includeSelfDM, + includePolicyReportFieldOptions, + policyReportFieldOptions, + recentlyUsedPolicyReportFieldOptions, + }, + ); } /** @@ -1942,8 +2003,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Record, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1951,24 +2012,27 @@ function getShareDestinationOptions( includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - maxRecentReportsToShow: 0, // Unlimited - includeRecentReports: true, - includeMultipleParticipantReports: true, - includePersonalDetails: false, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - excludeUnknownUsers, - includeSelfDM: true, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + maxRecentReportsToShow: 0, // Unlimited + includeRecentReports: true, + includeMultipleParticipantReports: true, + includePersonalDetails: false, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + excludeUnknownUsers, + includeSelfDM: true, + }, + ); } /** @@ -2001,20 +2065,23 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { * Build the options for the Workspace Member Invite view */ function getMemberInviteOptions( - personalDetails: OnyxEntry, + personalDetails: Array>, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false, ): GetOptions { - return getOptions({}, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - includePersonalDetails: true, - excludeLogins, - sortPersonalDetailsByAlphaAsc: true, - includeSelectedOptions, - }); + return getOptions( + {reports: [], personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + includePersonalDetails: true, + excludeLogins, + sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, + }, + ); } /** @@ -2154,8 +2221,10 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + createOptionList, + createOptionFromReport, getReportOption, getTaxRatesSection, }; -export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, GetOptions, OptionList, SearchOption, PayeePersonalDetails, Category}; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1973e665b20f..8b40ceafab65 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -15,7 +15,7 @@ function canUseDefaultRooms(betas: OnyxEntry): boolean { } function canUseReportFields(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); + return true; // !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); } function canUseViolations(betas: OnyxEntry): boolean { diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index c1c4717a295b..751813d1d3cf 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,9 +1,10 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; +import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -18,7 +19,6 @@ import doInteractionTask from '@libs/DoInteractionTask'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; @@ -29,9 +29,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft'; type NewChatPageWithOnyxProps = { - /** All reports shared with the user */ - reports: OnyxCollection; - /** New group chat draft data */ newGroupDraft: OnyxEntry; @@ -53,8 +50,9 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) { +function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) { const {translate} = useLocalize(); + const styles = useThemeStyles(); const personalData = useCurrentUserPersonalDetails(); @@ -72,13 +70,16 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF }; const [searchTerm, setSearchTerm] = useState(''); - const [filteredRecentReports, setFilteredRecentReports] = useState([]); - const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); - const [filteredUserToInvite, setFilteredUserToInvite] = useState(); + const [filteredRecentReports, setFilteredRecentReports] = useState([]); + const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); + const [filteredUserToInvite, setFilteredUserToInvite] = useState(); const [selectedOptions, setSelectedOptions] = useState(getGroupParticipants); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -91,8 +92,6 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())), ); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; @@ -145,8 +144,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports ?? [], + options.personalDetails ?? [], betas ?? [], searchTerm, newSelectedOptions, @@ -206,8 +205,8 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF personalDetails: newChatPersonalDetails, userToInvite, } = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports ?? [], + options.personalDetails ?? [], betas ?? [], searchTerm, selectedOptions, @@ -228,7 +227,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF setFilteredUserToInvite(userToInvite); // props.betas is not added as dependency since it doesn't change during the component lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reports, personalDetails, searchTerm]); + }, [options, searchTerm]); useEffect(() => { const interactionTask = doInteractionTask(() => { @@ -290,7 +289,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF headerMessage={headerMessage} boldStyle shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd} + shouldShowOptions={areOptionsInitialized} shouldShowConfirmButton shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} @@ -319,9 +318,6 @@ export default withOnyx({ newGroupDraft: { key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 77b5c48d8a72..49e53381e040 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -2,11 +2,10 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; @@ -25,30 +24,25 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy} from '@src/types/onyx'; +import type {Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; import SearchInputManager from './workspace/SearchInputManager'; -type RoomInvitePageOnyxProps = { - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; -}; - -type RoomInvitePageProps = RoomInvitePageOnyxProps & WithReportOrNotFoundProps & WithNavigationTransitionEndProps; +type RoomInvitePageProps = WithReportOrNotFoundProps & WithNavigationTransitionEndProps; type Sections = Array>>; -function RoomInvitePage({betas, personalDetails, report, policies, didScreenTransitionEnd}: RoomInvitePageProps) { +function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); + const {options, areOptionsInitialized} = useOptionsList(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -64,7 +58,7 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetails, betas ?? [], searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; @@ -83,12 +77,12 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran setInvitePersonalDetails(inviteOptions.personalDetails); setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetails, betas, searchTerm, excludedUsers]); + }, [betas, searchTerm, excludedUsers, options.personalDetails]); const sections = useMemo(() => { const sectionsArr: Sections = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return []; } @@ -130,7 +124,7 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran } return sectionsArr; - }, [invitePersonalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]); + }, [areOptionsInitialized, selectedOptions, searchTerm, invitePersonalDetails, userToInvite, translate]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -193,6 +187,7 @@ function RoomInvitePage({betas, personalDetails, report, policies, didScreenTran } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, Boolean(userToInvite), searchValue); }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + return ( ({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - })(RoomInvitePage), - ), -); +export default withNavigationTransitionEnd(withReportOrNotFound()(RoomInvitePage)); diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index b1555fd1cab8..c072bfd56913 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -1,10 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -17,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import * as ReportUtils from '@libs/ReportUtils'; +import type {OptionData} from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -30,9 +30,6 @@ type SearchPageOnyxProps = { /** Beta features list */ betas: OnyxEntry; - /** All reports shared with the user */ - reports: OnyxCollection; - /** Whether or not we are searching for reports on the server */ isSearchingForReports: OnyxEntry; }; @@ -40,7 +37,7 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; type SearchPageSectionItem = { - data: ReportUtils.OptionData[]; + data: OptionData[]; shouldShow: boolean; }; @@ -53,12 +50,14 @@ const setPerformanceTimersEnd = () => { const SearchPageFooterInstance = ; -function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchPageProps) { +function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const themeStyles = useThemeStyles(); - const personalDetails = usePersonalDetails(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: isScreenTransitionEnd, + }); const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -79,7 +78,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP userToInvite, headerMessage, } = useMemo(() => { - if (!isScreenTransitionEnd) { + if (!areOptionsInitialized) { return { recentReports: [], personalDetails: [], @@ -87,10 +86,10 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP headerMessage: '', }; } - const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas ?? []); - const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); - return {...options, headerMessage: header}; - }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []); + const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue); + return {...optionList, headerMessage: header}; + }, [areOptionsInitialized, options, debouncedSearchValue, betas]); const sections = useMemo((): SearchPageSectionList => { const newSections: SearchPageSectionList = []; @@ -119,7 +118,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP return newSections; }, [localPersonalDetails, recentReports, userToInvite]); - const selectReport = (option: ReportUtils.OptionData) => { + const selectReport = (option: OptionData) => { if (!option) { return; } @@ -136,8 +135,6 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP setIsScreenTransitionEnd(true); }; - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({safeAreaPaddingBottomStyle}) => ( <> - - sections={didScreenTransitionEnd && isOptionsDataReady ? sections : CONST.EMPTY_ARRAY} + + sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} ListItem={UserListItem} textInputValue={searchValue} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -164,7 +161,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined} onLayout={setPerformanceTimersEnd} onSelectRow={selectReport} - showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady} + showLoadingPlaceholder={!areOptionsInitialized} footerContent={SearchPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> @@ -178,9 +175,6 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP SearchPage.displayName = 'SearchPage'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 4870d39002ac..a05167d5cedf 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -19,8 +20,6 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -49,9 +48,6 @@ const propTypes = { }), ), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -68,7 +64,6 @@ const propTypes = { const defaultProps = { participants: [], safeAreaPaddingBottomStyle: {}, - reports: {}, betas: [], dismissedReferralBanners: {}, didScreenTransitionEnd: false, @@ -77,7 +72,6 @@ const defaultProps = { function MoneyTemporaryForRefactorRequestParticipantsSelector({ betas, participants, - reports, onFinish, onParticipantsAdded, safeAreaPaddingBottomStyle, @@ -93,6 +87,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -109,12 +106,12 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return [newSections, {}]; } const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports, + options.personalDetails, betas, debouncedSearchTerm, participants, @@ -175,7 +172,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [didScreenTransitionEnd, reports, personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, iouRequestType, maxParticipantsReached, translate]); + }, [ + areOptionsInitialized, + options.reports, + options.personalDetails, + betas, + debouncedSearchTerm, + participants, + iouType, + canUseP2PDistanceRequests, + iouRequestType, + maxParticipantsReached, + personalDetails, + translate, + ]); /** * Adds a single participant to the request @@ -342,13 +352,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( 0 ? safeAreaPaddingBottomStyle : {}]}> diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 16608ba13de8..05ef5baa8432 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; @@ -19,7 +20,6 @@ import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,9 +50,6 @@ const propTypes = { }), ), - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** padding bottom style of safe area */ safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), @@ -61,33 +58,26 @@ const propTypes = { /** Whether the money request is a distance request or not */ isDistanceRequest: PropTypes.bool, - - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, }; const defaultProps = { dismissedReferralBanners: {}, participants: [], safeAreaPaddingBottomStyle: {}, - reports: {}, betas: [], isDistanceRequest: false, - isSearchingForReports: false, }; function MoneyRequestParticipantsSelector({ betas, dismissedReferralBanners, participants, - reports, navigateToRequest, navigateToSplit, onAddParticipants, safeAreaPaddingBottomStyle, iouType, isDistanceRequest, - isSearchingForReports, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -96,6 +86,7 @@ function MoneyRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const {canUseP2PDistanceRequests} = usePermissions(); + const {options, areOptionsInitialized} = useOptionsList(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -104,8 +95,8 @@ function MoneyRequestParticipantsSelector({ const newChatOptions = useMemo(() => { const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, + options.reports, + options.personalDetails, betas, searchTerm, participants, @@ -132,7 +123,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]); + }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); /** * Returns the sections needed for the OptionsSelector @@ -365,7 +356,7 @@ function MoneyRequestParticipantsSelector({ onSelectRow={addSingleParticipant} footerContent={footerContent} headerMessage={headerMessage} - showLoadingPlaceholder={isSearchingForReports} + showLoadingPlaceholder={!areOptionsInitialized} rightHandSideComponent={itemRightSideComponent} /> @@ -380,14 +371,7 @@ export default withOnyx({ dismissedReferralBanners: { key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, })(MoneyRequestParticipantsSelector); diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 70c2d301b9ac..cee62380a011 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -11,46 +11,45 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {BaseShareLogListOnyxProps, BaseShareLogListProps} from './types'; -function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogListProps) { +function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState>({ recentReports: [], personalDetails: [], userToInvite: null, }); - const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); const isMounted = useRef(false); - const personalDetails = usePersonalDetails(); - + const {options, areOptionsInitialized} = useOptionsList(); const updateOptions = useCallback(() => { const { recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, - } = OptionsListUtils.getShareLogOptions(reports, personalDetails, searchValue.trim(), betas ?? []); + } = OptionsListUtils.getShareLogOptions(options, searchValue.trim(), betas ?? []); setSearchOptions({ recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, }); - }, [betas, personalDetails, reports, searchValue]); - - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); + }, [betas, options, searchValue]); useEffect(() => { + if (!areOptionsInitialized) { + return; + } + updateOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [options, areOptionsInitialized]); useEffect(() => { if (!isMounted.current) { @@ -126,7 +125,7 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis value={searchValue} headerMessage={headerMessage} showTitleTooltip - shouldShowOptions={isOptionsDataReady} + shouldShowOptions={areOptionsInitialized} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index bb199ddc905f..7a6ff74087de 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -7,7 +7,8 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useBetas, usePersonalDetails, useSession} from '@components/OnyxProvider'; +import {useBetas, useSession} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; @@ -39,22 +40,18 @@ type TaskAssigneeSelectorModalOnyxProps = { task: OnyxEntry; }; -type UseOptions = { - reports: OnyxCollection; -}; - type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps & WithNavigationTransitionEndProps; -function useOptions({reports}: UseOptions) { - const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; +function useOptions() { const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( - reports, - allPersonalDetails, + optionsList.reports, + optionsList.personalDetails, betas, debouncedSearchValue.trim(), [], @@ -87,18 +84,18 @@ function useOptions({reports}: UseOptions) { currentUserOption, headerMessage, }; - }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]); + }, [optionsList.reports, optionsList.personalDetails, betas, debouncedSearchValue, isLoading]); - return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; } -function TaskAssigneeSelectorModal({reports, task, didScreenTransitionEnd}: TaskAssigneeSelectorModalProps) { +function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); + const {userToInvite, recentReports, personalDetails, currentUserOption, searchValue, setSearchValue, headerMessage, areOptionsInitialized} = useOptions(); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); @@ -215,14 +212,14 @@ function TaskAssigneeSelectorModal({reports, task, didScreenTransitionEnd}: Task /> diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 5b56e58752ac..b4b8f9084a57 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,9 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -22,8 +22,6 @@ import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; type TaskShareDestinationSelectorModalOnyxProps = { - reports: OnyxCollection; - isSearchingForReports: OnyxEntry; }; @@ -40,29 +38,36 @@ const selectReportHandler = (option: unknown) => { Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reports: OnyxCollection) => - Object.keys(reports ?? {}).reduce((filtered, reportKey) => { - const report: OnyxEntry = reports?.[reportKey] ?? null; +const reportFilter = (reportOptions: Array>) => + (reportOptions ?? []).reduce((filtered: Array>, option) => { + const report = option.item; if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - return {...filtered, [reportKey]: report}; + filtered.push(option); } return filtered; - }, {}); + }, []); -function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { +function TaskShareDestinationSelectorModal({isSearchingForReports}: TaskShareDestinationSelectorModalProps) { + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); - const personalDetails = usePersonalDetails(); const {isOffline} = useNetwork(); + const {options: optionList, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); const textInputHint = useMemo(() => (isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''), [isOffline, translate]); const options = useMemo(() => { - const filteredReports = reportFilter(reports); - - const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); - + if (!areOptionsInitialized) { + return { + sections: [], + headerMessage: '', + }; + } + const filteredReports = reportFilter(optionList.reports); + const {recentReports} = OptionsListUtils.getShareDestinationOptions(filteredReports, optionList.personalDetails, [], debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, true); const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); const sections = @@ -84,7 +89,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas : []; return {sections, headerMessage}; - }, [personalDetails, reports, debouncedSearchValue]); + }, [areOptionsInitialized, optionList.reports, optionList.personalDetails, debouncedSearchValue]); useEffect(() => { ReportActions.searchInServer(debouncedSearchValue); @@ -94,29 +99,28 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas setDidScreenTransitionEnd(true)} > - {({didScreenTransitionEnd}) => ( - <> - Navigation.goBack(ROUTES.NEW_TASK)} + <> + Navigation.goBack(ROUTES.NEW_TASK)} + /> + + - - - - - )} + + ); } @@ -124,9 +128,6 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: Tas TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, isSearchingForReports: { key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 014097cd019c..3f95c3e02a5b 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -7,6 +7,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; @@ -75,6 +76,9 @@ function WorkspaceInvitePage({ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -98,8 +102,7 @@ function WorkspaceInvitePage({ const newPersonalDetailsDict: Record = {}; const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); - + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { @@ -150,12 +153,12 @@ function WorkspaceInvitePage({ setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); + }, [options.personalDetails, policyMembers, betas, searchTerm, excludedUsers]); const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; - if (!didScreenTransitionEnd) { + if (!areOptionsInitialized) { return []; } @@ -203,7 +206,7 @@ function WorkspaceInvitePage({ }); return sectionsArr; - }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); + }, [areOptionsInitialized, selectedOptions, searchTerm, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); @@ -304,7 +307,7 @@ function WorkspaceInvitePage({ onSelectRow={toggleOption} onConfirm={inviteUser} showScrollIndicator - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)} + showLoadingPlaceholder={!areOptionsInitialized} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} /> From 86a392b8e961b532ea2612baf8a58d25cc9bc73d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 16:17:37 +0500 Subject: [PATCH 13/16] remove unused code --- src/libs/Permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 8b40ceafab65..1973e665b20f 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -15,7 +15,7 @@ function canUseDefaultRooms(betas: OnyxEntry): boolean { } function canUseReportFields(betas: OnyxEntry): boolean { - return true; // !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); + return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas); } function canUseViolations(betas: OnyxEntry): boolean { From 928bfae1e01595762ce26ca8c71aff7f4f0b68ee Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 16:34:29 +0500 Subject: [PATCH 14/16] ts check --- src/pages/EditReportFieldDropdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx index 1d0247d0e3de..f61da2335a70 100644 --- a/src/pages/EditReportFieldDropdown.tsx +++ b/src/pages/EditReportFieldDropdown.tsx @@ -42,8 +42,8 @@ function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptio const validFieldOptions = fieldOptions?.filter((option) => !!option); const {policyReportFieldOptions} = OptionsListUtils.getFilteredOptions( - {}, - {}, + [], + [], [], debouncedSearchValue, [ From 936b9dc9ac705d8e0ca31cf82e50e05f60f3200d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 5 Apr 2024 02:29:25 +0500 Subject: [PATCH 15/16] add check for selected items --- src/components/SelectionList/types.ts | 2 +- src/pages/EditReportFieldDropdown.tsx | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index e401dd5456b2..62270e4ea64c 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -278,7 +278,7 @@ type BaseSelectionListProps = Partial & { isKeyboardShown?: boolean; /** Component to display on the right side of each child */ - rightHandSideComponent?: ((item: ListItem) => ReactElement) | ReactElement | null; + rightHandSideComponent?: ((item: ListItem) => ReactElement | null) | ReactElement | null; /** Whether to show the loading indicator for new options */ isLoadingNewOptions?: boolean; diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx index f61da2335a70..225051238e2b 100644 --- a/src/pages/EditReportFieldDropdown.tsx +++ b/src/pages/EditReportFieldDropdown.tsx @@ -1,10 +1,14 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentlyUsedReportFields} from '@src/types/onyx'; @@ -35,9 +39,26 @@ type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProp function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const theme = useTheme(); const {translate} = useLocalize(); const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]); + const itemRightSideComponent = useCallback( + (item: ListItem) => { + if (item.text === fieldValue) { + return ( + + ); + } + + return null; + }, + [theme.iconSuccessFill, fieldValue], + ); + const [sections, headerMessage] = useMemo(() => { const validFieldOptions = fieldOptions?.filter((option) => !!option); @@ -90,6 +111,7 @@ function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptio headerMessage={headerMessage} ListItem={RadioListItem} isRowMultilineSupported + rightHandSideComponent={itemRightSideComponent} /> ); } From 237cc5136bc89b9b22c7d22811673895b5890394 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 5 Apr 2024 02:38:50 +0500 Subject: [PATCH 16/16] fix ts --- src/components/SelectionList/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 62270e4ea64c..8e934d9f6490 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -33,7 +33,7 @@ type CommonListItemProps = { onDismissError?: (item: TItem) => void; /** Component to display on the right side */ - rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; + rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null; /** Styles for the pressable component */ pressableStyle?: StyleProp;