From 73d9a10a58b9fc5cda0dd6479f4acf635721f965 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 7 Feb 2024 20:48:26 +0500 Subject: [PATCH 01/46] 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/46] 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/46] 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/46] 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/46] 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 05dba39c4989e56d77bb23e1b77b396397b8243e Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 20 Mar 2024 13:38:32 +0000 Subject: [PATCH 06/46] Add themeing to default avatars. If no source provided, we use the default avatar --- assets/images/avatars/fallback-avatar.svg | 11 +++++++++- src/components/Avatar.tsx | 25 +++++++++++++++-------- src/libs/OptionsListUtils.ts | 2 +- src/styles/utils/index.ts | 13 ++++++++++-- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/assets/images/avatars/fallback-avatar.svg b/assets/images/avatars/fallback-avatar.svg index b4584d910190..69293d72aed9 100644 --- a/assets/images/avatars/fallback-avatar.svg +++ b/assets/images/avatars/fallback-avatar.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 2b2d0a60f657..5d740ed206b6 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -10,6 +10,7 @@ import type {AvatarSource} from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; import type {AvatarType} from '@src/types/onyx/OnyxCommon'; +import type {SVGAvatarColorStyle} from '@src/types'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Image from './Image'; @@ -74,18 +75,25 @@ function Avatar({ setImageError(false); }, [source]); - if (!source) { - return null; - } - const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; - const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill; - const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; + // We pass the color styles down to the SVG for the workspace and fallback avatar. + const useFallBackAvatar = imageError || source === Expensicons.FallbackAvatar; + + let iconColors: SVGAvatarColorStyle; + if (isWorkspace) { + iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(name); + } else if (useFallBackAvatar) { + iconColors = StyleUtils.getBackgroundColorAndFill(theme.border, theme.icon); + } else { + iconColors = null; + } + + const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : (fallbackIcon || Expensicons.FallbackAvatar); const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; const avatarSource = imageError ? fallbackAvatar : source; @@ -107,11 +115,10 @@ function Avatar({ src={avatarSource} height={iconSize} width={iconSize} - fill={imageError ? theme.offline : iconFillColor} + fill={imageError ? theme.offline : null} additionalStyles={[ StyleUtils.getAvatarBorderStyle(size, type), - isWorkspace && StyleUtils.getDefaultWorkspaceAvatarColor(name), - imageError && StyleUtils.getBackgroundColorStyle(theme.fallbackIconColor), + iconColors, iconAdditionalStyles, ]} /> diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index bacd019904a3..638b29633cdd 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1685,7 +1685,7 @@ function getOptions( // If user doesn't exist, use a default avatar userToInvite.icons = [ { - source: UserUtils.getAvatar('', optimisticAccountID), + source: '', name: searchValue, type: CONST.ICON_TYPE_AVATAR, }, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 9a25313837fe..ba2da71bce59 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -39,10 +39,10 @@ import type { EreceiptColorStyle, ParsableStyle, TextColorStyle, - WorkspaceColorStyle, + SVGAvatarColorStyle, } from './types'; -const workspaceColorOptions: WorkspaceColorStyle[] = [ +const workspaceColorOptions: SVGAvatarColorStyle[] = [ {backgroundColor: colors.blue200, fill: colors.blue700}, {backgroundColor: colors.blue400, fill: colors.blue800}, {backgroundColor: colors.blue700, fill: colors.blue200}, @@ -276,6 +276,14 @@ function getDefaultWorkspaceAvatarColor(workspaceName: string): ViewStyle { return workspaceColorOptions[colorHash]; } + +/** + * Helper method to return formatted backgroundColor and fill styles + */ +function getBackgroundColorAndFill(backgroundColor: string, fill: string): SVGAvatarColorStyle { + return {backgroundColor, fill}; +} + /** * Helper method to return eReceipt color code */ @@ -1112,6 +1120,7 @@ const staticStyleUtils = { getComposeTextAreaPadding, getColorStyle, getDefaultWorkspaceAvatarColor, + getBackgroundColorAndFill, getDirectionStyle, getDropDownButtonHeight, getEmojiPickerListHeight, From 5fd775bcb21560bc8c31235c0f4ee7c852f43e6b Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 20 Mar 2024 14:46:10 +0000 Subject: [PATCH 07/46] update cases for fallback avatar --- src/components/Avatar.tsx | 2 +- src/libs/OptionsListUtils.ts | 5 +++-- src/libs/UserUtils.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 5d740ed206b6..534ed877d997 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -115,7 +115,7 @@ function Avatar({ src={avatarSource} height={iconSize} width={iconSize} - fill={imageError ? theme.offline : null} + fill={imageError ? theme.offline : fill} additionalStyles={[ StyleUtils.getAvatarBorderStyle(size, type), iconColors, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 638b29633cdd..851fb317461e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -34,6 +34,7 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; +import {FallbackAvatar} from '@components/Icon/Expensicons'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; @@ -1682,10 +1683,10 @@ function getOptions( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing userToInvite.alternateText = userToInvite.alternateText || searchValue; - // If user doesn't exist, use a default avatar + // If user doesn't exist, always use the fallback avatar userToInvite.icons = [ { - source: '', + source: FallbackAvatar, name: searchValue, type: CONST.ICON_TYPE_AVATAR, }, diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 147343e99ceb..d2d45a4d8490 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -83,7 +83,7 @@ function generateAccountID(searchValue: string): number { * @returns */ function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset { - if (accountID <= 0) { + if (!accountID || accountID <= 0) { return FallbackAvatar; } if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { From 6c85462631e56436b6350cb36b603b872b493567 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 20 Mar 2024 15:01:19 +0000 Subject: [PATCH 08/46] prettier and type fix --- src/components/Avatar.tsx | 10 +++------- src/libs/OptionsListUtils.ts | 2 +- src/styles/utils/index.ts | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 534ed877d997..412918fa52c7 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -9,8 +9,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; +import type {SVGAvatarColorStyle} from '@src/styles/utils/types'; import type {AvatarType} from '@src/types/onyx/OnyxCommon'; -import type {SVGAvatarColorStyle} from '@src/types'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Image from './Image'; @@ -93,7 +93,7 @@ function Avatar({ iconColors = null; } - const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : (fallbackIcon || Expensicons.FallbackAvatar); + const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; const avatarSource = imageError ? fallbackAvatar : source; @@ -116,11 +116,7 @@ function Avatar({ height={iconSize} width={iconSize} fill={imageError ? theme.offline : fill} - additionalStyles={[ - StyleUtils.getAvatarBorderStyle(size, type), - iconColors, - iconAdditionalStyles, - ]} + additionalStyles={[StyleUtils.getAvatarBorderStyle(size, type), iconColors, iconAdditionalStyles]} /> )} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 851fb317461e..7184394fbf39 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,6 +7,7 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {FallbackAvatar} from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -34,7 +35,6 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index ba2da71bce59..81226233804a 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -38,8 +38,8 @@ import type { EReceiptColorName, EreceiptColorStyle, ParsableStyle, - TextColorStyle, SVGAvatarColorStyle, + TextColorStyle, } from './types'; const workspaceColorOptions: SVGAvatarColorStyle[] = [ @@ -276,7 +276,6 @@ function getDefaultWorkspaceAvatarColor(workspaceName: string): ViewStyle { return workspaceColorOptions[colorHash]; } - /** * Helper method to return formatted backgroundColor and fill styles */ From 04d24d2c59782efd3ccd561691ee8fa4259b854a Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 21 Mar 2024 11:01:18 +0000 Subject: [PATCH 09/46] Add missing type file changes --- src/styles/utils/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles/utils/types.ts b/src/styles/utils/types.ts index 5fe844f0f358..7dc70dc2e64e 100644 --- a/src/styles/utils/types.ts +++ b/src/styles/utils/types.ts @@ -40,7 +40,7 @@ type ButtonSizeValue = ValueOf; type ButtonStateName = ValueOf; type AvatarSize = {width: number}; -type WorkspaceColorStyle = {backgroundColor: ColorValue; fill: ColorValue}; +type SVGAvatarColorStyle = {backgroundColor: ColorValue; fill: ColorValue}; type EreceiptColorStyle = {backgroundColor: ColorValue; color: ColorValue}; type TextColorStyle = {color: string}; @@ -55,7 +55,7 @@ export type { ButtonSizeValue, ButtonStateName, AvatarSize, - WorkspaceColorStyle, + SVGAvatarColorStyle, EreceiptColorStyle, TextColorStyle, }; From 840297c84c7bbed3b4bf4113a9141cda84b6b939 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 10:15:57 +0000 Subject: [PATCH 10/46] type --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 412918fa52c7..e3c420c57d8f 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -84,7 +84,7 @@ function Avatar({ // We pass the color styles down to the SVG for the workspace and fallback avatar. const useFallBackAvatar = imageError || source === Expensicons.FallbackAvatar; - let iconColors: SVGAvatarColorStyle; + let iconColors; if (isWorkspace) { iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(name); } else if (useFallBackAvatar) { From 7ba8e43923cc27b53ff173c78ef8958db40f2f17 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 10:22:50 +0000 Subject: [PATCH 11/46] typescript --- src/components/Avatar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index e3c420c57d8f..b8471e732928 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -8,8 +8,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; +import type IconAsset from '@src/types/utils/IconAsset'; import CONST from '@src/CONST'; -import type {SVGAvatarColorStyle} from '@src/styles/utils/types'; import type {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -40,7 +40,7 @@ type AvatarProps = { /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. */ - fallbackIcon?: AvatarSource; + fallbackIcon?: IconAsset; /** Used to locate fallback icon in end-to-end tests. */ fallbackIconTestID?: string; @@ -96,7 +96,7 @@ function Avatar({ const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; - const avatarSource = imageError ? fallbackAvatar : source; + const avatarSource = (imageError || !source) ? fallbackAvatar : source; return ( From 07b15ecbd0be720948f795404325acaa5f5fe75e Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 10:27:22 +0000 Subject: [PATCH 12/46] undo type change --- src/components/Avatar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index b8471e732928..95cb670987f0 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -8,7 +8,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; -import type IconAsset from '@src/types/utils/IconAsset'; import CONST from '@src/CONST'; import type {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; @@ -40,7 +39,7 @@ type AvatarProps = { /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. */ - fallbackIcon?: IconAsset; + fallbackIcon?: AvatarSource; /** Used to locate fallback icon in end-to-end tests. */ fallbackIconTestID?: string; From 6ab91c0283776c4dd85aceb10bb0d18b2605121d Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 10:31:04 +0000 Subject: [PATCH 13/46] prevent flashing fill color for loading workspace/fallback svg --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 95cb670987f0..34153624237b 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -114,7 +114,7 @@ function Avatar({ src={avatarSource} height={iconSize} width={iconSize} - fill={imageError ? theme.offline : fill} + fill={imageError ? iconColors?.fill ?? theme.offline : fill} additionalStyles={[StyleUtils.getAvatarBorderStyle(size, type), iconColors, iconAdditionalStyles]} /> From 228653d414c2bbbc2f316e70fea7491598430be0 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 10:39:04 +0000 Subject: [PATCH 14/46] prettier --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 34153624237b..172f51b8b04f 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -95,7 +95,7 @@ function Avatar({ const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; - const avatarSource = (imageError || !source) ? fallbackAvatar : source; + const avatarSource = imageError || !source ? fallbackAvatar : source; return ( From 44dc9fcc28508230324dfcacfdf2d12235e36329 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Tue, 26 Mar 2024 13:41:42 +0000 Subject: [PATCH 15/46] revert change to optionListUtils to handle in a separate PR --- src/libs/OptionsListUtils.ts | 5 ++--- src/libs/UserUtils.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1656190777d1..0f83b260c8f2 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,7 +7,6 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -1715,10 +1714,10 @@ function getOptions( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing userToInvite.alternateText = userToInvite.alternateText || searchValue; - // If user doesn't exist, always use the fallback avatar + // If user doesn't exist, use a default avatar userToInvite.icons = [ { - source: FallbackAvatar, + source: UserUtils.getAvatar('', optimisticAccountID), name: searchValue, type: CONST.ICON_TYPE_AVATAR, }, diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index d2d45a4d8490..147343e99ceb 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -83,7 +83,7 @@ function generateAccountID(searchValue: string): number { * @returns */ function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset { - if (!accountID || accountID <= 0) { + if (accountID <= 0) { return FallbackAvatar; } if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { From 988ee9a61cfd4be2f09c7329d5b778b8a0b56a33 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Tue, 26 Mar 2024 13:45:19 +0000 Subject: [PATCH 16/46] clean up fallback logic --- src/components/Avatar.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 172f51b8b04f..a37a587f6820 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -81,7 +81,11 @@ function Avatar({ const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; // We pass the color styles down to the SVG for the workspace and fallback avatar. - const useFallBackAvatar = imageError || source === Expensicons.FallbackAvatar; + const useFallBackAvatar = imageError || source === Expensicons.FallbackAvatar || !source; + const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; + const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; + const avatarSource = useFallBackAvatar ? fallbackAvatar : source; + let iconColors; if (isWorkspace) { @@ -92,11 +96,6 @@ function Avatar({ iconColors = null; } - const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; - const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; - - const avatarSource = imageError || !source ? fallbackAvatar : source; - return ( {typeof avatarSource === 'string' ? ( From 419ac90fa0b7267fa34329bd1ee6873c071d35d6 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Tue, 26 Mar 2024 13:46:10 +0000 Subject: [PATCH 17/46] prettier --- src/components/Avatar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index a37a587f6820..4f353358381d 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -86,7 +86,6 @@ function Avatar({ const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; const avatarSource = useFallBackAvatar ? fallbackAvatar : source; - let iconColors; if (isWorkspace) { iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(name); From 72e9d757c5abc6462361175bff13b04b9263252a Mon Sep 17 00:00:00 2001 From: Georgia Monahan <38015950+grgia@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:47:41 +0000 Subject: [PATCH 18/46] Update src/components/Avatar.tsx Co-authored-by: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 4f353358381d..ce753285ce39 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -112,7 +112,7 @@ function Avatar({ src={avatarSource} height={iconSize} width={iconSize} - fill={imageError ? iconColors?.fill ?? theme.offline : fill} + fill={imageError ? theme.offline : iconColors?.fill ?? fill} additionalStyles={[StyleUtils.getAvatarBorderStyle(size, type), iconColors, iconAdditionalStyles]} /> From 7950537845073ec3d324ee5d9d475742be67ebd7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 27 Mar 2024 01:28:35 +0500 Subject: [PATCH 19/46] 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 20/46] 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 68d374f219d3d9810ee2cb18940ea0389c1ea502 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 28 Mar 2024 11:06:40 +0000 Subject: [PATCH 21/46] change avatar loading BG to borders color --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index ce753285ce39..c2b8e3e27567 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -112,7 +112,7 @@ function Avatar({ src={avatarSource} height={iconSize} width={iconSize} - fill={imageError ? theme.offline : iconColors?.fill ?? fill} + fill={imageError ? theme.border : iconColors?.fill ?? fill} additionalStyles={[StyleUtils.getAvatarBorderStyle(size, type), iconColors, iconAdditionalStyles]} /> From 3679762d5472a1793f6cb49b2e35d08eab78727d Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 28 Mar 2024 14:03:41 +0000 Subject: [PATCH 22/46] change getAvatarStyle to use border color too --- src/styles/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 5346b32728fd..9d1b371b43c3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1239,7 +1239,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ height: avatarSize, width: avatarSize, borderRadius: avatarSize, - backgroundColor: theme.offline, + backgroundColor: theme.border, }; }, From b6f8d0bbca2f1f5fbb71344117b4cfc348ffb039 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 10:26:26 +0500 Subject: [PATCH 23/46] 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 24/46] 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 25/46] 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 26/46] 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 27/46] 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 28/46] 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 29/46] 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 4288aed65ed8d4536de2272dfe142c1d0766e8e2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 4 Apr 2024 22:10:21 +0800 Subject: [PATCH 30/46] fix top bar doesn't rerender when session changes --- .../createCustomBottomTabNavigator/TopBar.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 38bfe4af9ab6..84d427389920 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -17,19 +17,21 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy} from '@src/types/onyx'; +import type {Policy, Session as SessionType} from '@src/types/onyx'; type TopBarOnyxProps = { policy: OnyxEntry; + session: OnyxEntry; }; // eslint-disable-next-line react/no-unused-prop-types type TopBarProps = {activeWorkspaceID?: string} & TopBarOnyxProps; -function TopBar({policy}: TopBarProps) { +function TopBar({policy, session}: TopBarProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); + const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; const headerBreadcrumb = policy?.name ? {type: CONST.BREADCRUMB_TYPE.STRONG, text: policy.name} @@ -57,7 +59,7 @@ function TopBar({policy}: TopBarProps) { /> - {Session.isAnonymousUser() ? ( + {isAnonymousUser ? ( ) : ( @@ -84,4 +86,7 @@ export default withOnyx({ policy: { key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, }, + session: { + key: ONYXKEYS.SESSION, + }, })(TopBar); From 936b9dc9ac705d8e0ca31cf82e50e05f60f3200d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 5 Apr 2024 02:29:25 +0500 Subject: [PATCH 31/46] 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 32/46] 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; From de5d16cb98b9db1e193bbeb283208d3c7753aea4 Mon Sep 17 00:00:00 2001 From: Lauren Schurr <33293730+lschurr@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:55:48 -0700 Subject: [PATCH 33/46] Create Expense-Rules.md --- .../expenses/Expense-Rules.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/articles/expensify-classic/expenses/Expense-Rules.md diff --git a/docs/articles/expensify-classic/expenses/Expense-Rules.md b/docs/articles/expensify-classic/expenses/Expense-Rules.md new file mode 100644 index 000000000000..295aa8d00cc9 --- /dev/null +++ b/docs/articles/expensify-classic/expenses/Expense-Rules.md @@ -0,0 +1,55 @@ +--- +title: Expense Rules +description: Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant's name. + +--- +# Overview +Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant’s name. + +# How to use Expense Rules +**To create an expense rule, follow these steps:** +1. Navigate to **Settings > Account > Expense Rules** +2. Click on **New Rule** +3. Fill in the required information to set up your rule + +When creating an expense rule, you will be able to apply the following rules to expenses: + +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_01.png){:width="100%"} + +- **Merchant:** Updates the merchant name, e.g., “Starbucks #238” could be changed to “Starbucks” +- **Category:** Applies a workspace category to the expense +- **Tag:** Applies a tag to the expense, e.g., a Department or Location +- **Description:** Adds a description to the description field on the expense +- **Reimbursability:** Determines whether the expense will be marked as reimbursable or non-reimbursable +- **Billable**: Determines whether the expense is billable +- **Add to a report named:** Adds the expense to a report with the name you type into the field. If no report with that name exists, a new report will be created + +## Tips on using Expense Rules +- If you'd like to apply a rule to all expenses (“Universal Rule”) rather than just one merchant, simply enter a period [.] and nothing else into the **“When the merchant name contains:”** field. **Note:** Universal Rules will always take precedence over all other rules for category (more on this below). +- You can apply a rule to previously entered expenses by checking the **Apply to existing matching expenses** checkbox. Click “Preview Matching Expenses” to see if your rule matches the intended expenses. +- You can create expense rules while editing an expense. To do this, simply check the box **“Create a rule based on your changes"** at the time of editing. Note that the expense must be saved, reopened, and edited for this option to appear. + + +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_02.png){:width="100%"} + + +To delete an expense rule, go to **Settings > Account > Expense Rules**, scroll down to the rule you’d like to remove, and then click the trash can icon in the upper right corner of the rule: + +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_03.png){:width="100%"} + +# Deep Dive +In general, your expense rules will be applied in order, from **top to bottom**, i.e., from the first rule. However, other settings can impact how expense rules are applied. Here is the hierarchy that determines how these are applied: +1. A Universal Rule will **always** precede over any other expense category rules. Rules that would otherwise change the expense category will **not** override the Universal Rule. +2. If Scheduled Submit and the setting “Enforce Default Report Title” are enabled on the workspace, this will take precedence over any rules trying to add the expense to a report. +3. If the expense is from a Company Card that is forced to a workspace with strict rule enforcement, those rules will take precedence over individual expense rules. +4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. + + +{% include faq-begin.md %} +## How can I use Expense Rules to vendor match when exporting to an accounting package? +When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. +When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. +For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. +This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. + +{% include faq-end.md %} From 8643d4105e2e74ee14b9b820b89d6176aaef212b Mon Sep 17 00:00:00 2001 From: Lauren Schurr <33293730+lschurr@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:02:24 -0700 Subject: [PATCH 34/46] Delete docs/articles/expensify-classic/reports/Expense-Rules.md --- .../reports/Expense-Rules.md | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 docs/articles/expensify-classic/reports/Expense-Rules.md diff --git a/docs/articles/expensify-classic/reports/Expense-Rules.md b/docs/articles/expensify-classic/reports/Expense-Rules.md deleted file mode 100644 index 295aa8d00cc9..000000000000 --- a/docs/articles/expensify-classic/reports/Expense-Rules.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Expense Rules -description: Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant's name. - ---- -# Overview -Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant’s name. - -# How to use Expense Rules -**To create an expense rule, follow these steps:** -1. Navigate to **Settings > Account > Expense Rules** -2. Click on **New Rule** -3. Fill in the required information to set up your rule - -When creating an expense rule, you will be able to apply the following rules to expenses: - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_01.png){:width="100%"} - -- **Merchant:** Updates the merchant name, e.g., “Starbucks #238” could be changed to “Starbucks” -- **Category:** Applies a workspace category to the expense -- **Tag:** Applies a tag to the expense, e.g., a Department or Location -- **Description:** Adds a description to the description field on the expense -- **Reimbursability:** Determines whether the expense will be marked as reimbursable or non-reimbursable -- **Billable**: Determines whether the expense is billable -- **Add to a report named:** Adds the expense to a report with the name you type into the field. If no report with that name exists, a new report will be created - -## Tips on using Expense Rules -- If you'd like to apply a rule to all expenses (“Universal Rule”) rather than just one merchant, simply enter a period [.] and nothing else into the **“When the merchant name contains:”** field. **Note:** Universal Rules will always take precedence over all other rules for category (more on this below). -- You can apply a rule to previously entered expenses by checking the **Apply to existing matching expenses** checkbox. Click “Preview Matching Expenses” to see if your rule matches the intended expenses. -- You can create expense rules while editing an expense. To do this, simply check the box **“Create a rule based on your changes"** at the time of editing. Note that the expense must be saved, reopened, and edited for this option to appear. - - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_02.png){:width="100%"} - - -To delete an expense rule, go to **Settings > Account > Expense Rules**, scroll down to the rule you’d like to remove, and then click the trash can icon in the upper right corner of the rule: - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_03.png){:width="100%"} - -# Deep Dive -In general, your expense rules will be applied in order, from **top to bottom**, i.e., from the first rule. However, other settings can impact how expense rules are applied. Here is the hierarchy that determines how these are applied: -1. A Universal Rule will **always** precede over any other expense category rules. Rules that would otherwise change the expense category will **not** override the Universal Rule. -2. If Scheduled Submit and the setting “Enforce Default Report Title” are enabled on the workspace, this will take precedence over any rules trying to add the expense to a report. -3. If the expense is from a Company Card that is forced to a workspace with strict rule enforcement, those rules will take precedence over individual expense rules. -4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. - - -{% include faq-begin.md %} -## How can I use Expense Rules to vendor match when exporting to an accounting package? -When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. -When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. -For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. -This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. - -{% include faq-end.md %} From 4a0e8220ebdfdb68ac2de44e22a5787c4e2d60c0 Mon Sep 17 00:00:00 2001 From: Lauren Schurr <33293730+lschurr@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:07:36 -0700 Subject: [PATCH 35/46] Create Currency.md --- .../expensify-classic/workspaces/Currency.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/articles/expensify-classic/workspaces/Currency.md diff --git a/docs/articles/expensify-classic/workspaces/Currency.md b/docs/articles/expensify-classic/workspaces/Currency.md new file mode 100644 index 000000000000..77b5fbbb3ebc --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Currency.md @@ -0,0 +1,63 @@ +--- +title: Report Currency +description: Understanding expense and report currency +--- + +# Overview +As a workspace admin, you can choose a default currency for your employees' expense reports, and we’ll automatically convert any expenses into that currency. + +Here are a few essential things to remember: + +- Currency settings for a workspace apply to all expenses under that workspace. If you need different default currencies for certain employees, creating separate workspaces and configuring the currency settings is best. +- As an admin, the currency settings you establish in the workspace will take precedence over any currency settings individual users may have in their accounts. +- Currency is a workspace-level setting, meaning the currency you set will determine the currency for all expenses submitted on that workspace. + +# How to select the currency on a workspace + +## As an admin on a group workspace + +1. Sign into your Expensify web account +2. Go to **Settings > Workspaces > Group > _[Workspace Name]_> Reports > Report Basics** +3. Adjust the **Report Output Currency** + +## On an individual workspace + +1. Sign into your Expensify web account +2. Go to **Settings > Workspaces > Individual >_[Workspace Name]_> Reports > Report Basics** +3. Adjust the **Report Output Currency** + +Please note the currency setting on an individual workspace is overridden when you submit a report on a group workspace. + +# Deep Dive + +## Conversion Rates + +Using data from Open Exchange Rates, Expensify takes the average rate on the day the expense occurred to convert an expense from one currency to another. The conversion rate can vary depending on when the expense happened since the rate is determined after the market closes on that specific date. + +If the markets aren’t open on the day the expense takes place (i.e., on a Saturday), Expensify will use the daily average rate from the last available market day before the purchase took place. + +When an expense is logged for a future date, possibly to anticipate a purchase that has yet to occur, we'll use the most recent available data. This means the report's value may change up to the day of that expense. + +## Managing expenses for employees in several different countries + +Suppose you have employees scattered across the globe who submit expense reports in various currencies. The best way to manage those expenses is to create separate group workspaces for each location or region where your employees are based. + +Then, set the default currency for that workspace to match the currency in which the employees are reimbursed. + +For example, if you have employees in the US, France, Japan, and India, you’d want to create four separate workspaces, add the employees to each, and then set the corresponding currency for each workspace. + +{% include faq-begin.md %} + +## I have expenses in several different currencies. How will this show up on a report? + +If you're traveling to foreign countries during a reporting period and making purchases in various currencies, each expense is imported with the currency of the purchase. + +On your expense report, Expensify will automatically convert each expense to the default currency set for the group workspace. + +## How does the currency of an expense impact the conversion rate? + +Expenses entered in a foreign currency are automatically converted to the default currency on your workspace. The conversion uses the day’s average trading rate pulled from [Open Exchange Rates](https://openexchangerates.org/). + +If you want to bypass the exchange rate conversion, you can manually enter an expense in your default currency instead. + +{% include faq-end.md %} From 8baf9593d1eb0c3a018bb3de75affaf3d79d8ea9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 5 Apr 2024 11:45:30 +0800 Subject: [PATCH 36/46] show split bill option for dm --- src/libs/ReportUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9d7b6b1d6549..032ecd7334a3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4828,7 +4828,6 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry currentUserPersonalDetails?.accountID !== accountID); const hasSingleOtherParticipantInReport = otherParticipants.length === 1; - const hasMultipleOtherParticipants = otherParticipants.length > 1; let options: Array> = []; if (isSelfDM(report)) { @@ -4837,11 +4836,11 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 0) || - (isDM(report) && hasMultipleOtherParticipants) || + (isDM(report) && otherParticipants.length > 0) || (isGroupChat(report) && otherParticipants.length > 0) || (isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat) ) { From 49cbef0ec9b226c1a8437d02981aa51bae3abd8d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 5 Apr 2024 11:46:12 +0800 Subject: [PATCH 37/46] update comment --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 032ecd7334a3..0e0dce69614e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4803,6 +4803,7 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o * - Send option should show for: * - DMs * - Split options should show for: + * - DMs * - chat/ policy rooms with more than 1 participants * - groups chats with 3 and more participants * - corporate workspace chats From ed035576d93610914fb05b4bb0cb8179760dcaa9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 5 Apr 2024 13:31:03 +0800 Subject: [PATCH 38/46] update test to correctly reflect the new expected behavior --- tests/unit/ReportUtilsTest.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index ffd5c9147dc0..10a50fa31869 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -597,7 +597,8 @@ describe('ReportUtils', () => { type: CONST.REPORT.TYPE.CHAT, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); - expect(moneyRequestOptions.length).toBe(2); + expect(moneyRequestOptions.length).toBe(3); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); }); From 7c7229f2aff72c1237a001b1be5953ca87554153 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 5 Apr 2024 10:54:58 +0100 Subject: [PATCH 39/46] if we've calculated the fill (which we do for imageError) then use that --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index c2b8e3e27567..358f5333bfba 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -112,7 +112,7 @@ function Avatar({ src={avatarSource} height={iconSize} width={iconSize} - fill={imageError ? theme.border : iconColors?.fill ?? fill} + fill={imageError ? iconColors?.fill ?? theme.offline : iconColors?.fill ?? fill} additionalStyles={[StyleUtils.getAvatarBorderStyle(size, type), iconColors, iconAdditionalStyles]} /> From 479efd67a4e634d34d391f0c489690be517152f5 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 5 Apr 2024 15:01:07 +0200 Subject: [PATCH 40/46] Remove OptionsSelector part 1 --- src/components/SelectionList/BaseListItem.tsx | 9 ++++ .../SelectionList/BaseSelectionList.tsx | 25 +++++++--- src/components/SelectionList/types.ts | 19 +++++-- src/components/TagPicker/index.tsx | 22 +++------ src/libs/OptionsListUtils.ts | 15 +++--- src/pages/EditReportFieldDropdownPage.tsx | 29 +++++------ src/pages/WorkspaceSwitcherPage.tsx | 49 ++++++------------- .../ShareLogList/BaseShareLogList.tsx | 49 +++++++++---------- tests/unit/OptionsListUtilsTest.js | 13 +++++ 9 files changed, 121 insertions(+), 109 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index cd1a40b5ef5d..038a1dbcec8d 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -81,6 +81,15 @@ function BaseListItem({ )} + {!item.isSelected && !!item.brickRoadIndicator && ( + + + + )} + {rightHandSideComponentRender()} {FooterComponent} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 8dd7577de779..80d6ce122719 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -71,6 +71,9 @@ function BaseSelectionList( textInputRef, headerMessageStyle, shouldHideListOnInitialRender = true, + textInputIconLeft, + sectionTitleStyles, + textInputAutoFocus = true, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -79,7 +82,7 @@ function BaseSelectionList( const listRef = useRef>>(null); const innerTextInputRef = useRef(null); const focusTimeoutRef = useRef(null); - const shouldShowTextInput = !!textInputLabel; + const shouldShowTextInput = !!textInputLabel || !!textInputIconLeft; const shouldShowSelectAll = !!onSelectAll; const activeElementRole = useActiveElementRole(); const isFocused = useIsFocused(); @@ -310,7 +313,7 @@ function BaseSelectionList( // We do this so that we can reference the height in `getItemLayout` – // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. - + {section.title} ); @@ -377,6 +380,9 @@ function BaseSelectionList( /** Focuses the text input when the component comes into focus and after any navigation animations finish. */ useFocusEffect( useCallback(() => { + if (!textInputAutoFocus) { + return; + } if (shouldShowTextInput) { focusTimeoutRef.current = setTimeout(() => { if (!innerTextInputRef.current) { @@ -391,7 +397,7 @@ function BaseSelectionList( } clearTimeout(focusTimeoutRef.current); }; - }, [shouldShowTextInput]), + }, [shouldShowTextInput, textInputAutoFocus]), ); const prevTextInputValue = usePrevious(textInputValue); @@ -494,8 +500,12 @@ function BaseSelectionList( return; } - // eslint-disable-next-line no-param-reassign - textInputRef.current = element as RNTextInput; + if (typeof textInputRef === 'function') { + textInputRef(element as RNTextInput); + } else { + // eslint-disable-next-line no-param-reassign + textInputRef.current = element as RNTextInput; + } }} label={textInputLabel} accessibilityLabel={textInputLabel} @@ -508,6 +518,7 @@ function BaseSelectionList( inputMode={inputMode} selectTextOnFocus spellCheck={false} + iconLeft={textInputIconLeft} onSubmitEditing={selectFocusedOption} blurOnSubmit={!!flattenedSections.allOptions.length} isLoading={isLoadingNewOptions} @@ -515,7 +526,9 @@ function BaseSelectionList( /> )} - {!!headerMessage && ( + {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} + {/* This is misleading because we might be in the process of loading fresh options from the server. */} + {!isLoadingNewOptions && !!headerMessage && ( {headerMessage} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index e401dd5456b2..f4b1b990811f 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,10 +1,12 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; import type {MaybePhraseKey} from '@libs/Localize'; +import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import type CONST from '@src/CONST'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type IconAsset from '@src/types/utils/IconAsset'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; @@ -110,6 +112,8 @@ type ListItem = { /** The search value from the selection list */ searchText?: string | null; + + brickRoadIndicator?: BrickRoad | '' | null; }; type ListItemProps = CommonListItemProps & { @@ -214,6 +218,12 @@ type BaseSelectionListProps = Partial & { /** Max length for the text input */ textInputMaxLength?: number; + /** Icon to display on the left side of TextInput */ + textInputIconLeft?: IconAsset; + + /** Whether text input should be focused */ + textInputAutoFocus?: boolean; + /** Callback to fire when the text input changes */ onChangeText?: (text: string) => void; @@ -221,7 +231,7 @@ type BaseSelectionListProps = Partial & { inputMode?: InputModeOptions; /** Item `keyForList` to focus initially */ - initiallyFocusedOptionKey?: string; + initiallyFocusedOptionKey?: string | null; /** Callback to fire when the list is scrolled */ onScroll?: () => void; @@ -272,7 +282,7 @@ type BaseSelectionListProps = Partial & { disableKeyboardShortcuts?: boolean; /** Styles to apply to SelectionList container */ - containerStyle?: ViewStyle; + containerStyle?: StyleProp; /** Whether keyboard is visible on the screen */ isKeyboardShown?: boolean; @@ -296,7 +306,10 @@ type BaseSelectionListProps = Partial & { isRowMultilineSupported?: boolean; /** Ref for textInput */ - textInputRef?: MutableRefObject; + textInputRef?: MutableRefObject | ((ref: TextInput | null) => void); + + /** Styles for the section title */ + sectionTitleStyles?: StyleProp; /** * When true, the list won't be visible until the list layout is measured. This prevents the list from "blinking" as it's scrolled to the bottom which is recommended for large lists. diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 54ad016173b7..cd462dbe3f2e 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -2,7 +2,8 @@ import React, {useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; -import OptionsSelector from '@components/OptionsSelector'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -100,22 +101,15 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; return ( - >): Option[] { +function getTagsOptions(tags: Array>, selectedOptions?: SelectedTagOption[]): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1115,6 +1115,7 @@ function getTagsOptions(tags: Array>): Optio searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, + isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), }; }); } @@ -1146,7 +1147,7 @@ function getTagListSections( // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedTagOptions), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); return tagSections; @@ -1159,7 +1160,7 @@ function getTagListSections( // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags), + data: getTagsOptions(searchTags, selectedOptions), }); return tagSections; @@ -1170,7 +1171,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags), + data: getTagsOptions(enabledTags, selectedOptions), }); return tagSections; @@ -1195,7 +1196,7 @@ function getTagListSections( // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedTagOptions), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); } @@ -1206,7 +1207,7 @@ function getTagListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getTagsOptions(cutRecentlyUsedTags), + data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); } @@ -1214,7 +1215,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags), + data: getTagsOptions(filteredTags, selectedOptions), }); return tagSections; diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index e887860ae155..75c2a9c5be26 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -2,8 +2,9 @@ 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 OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -42,6 +43,7 @@ type ReportFieldDropdownData = { keyForList: string; searchText: string; tooltipText: string; + isSelected?: boolean; }; type ReportFieldDropdownSectionItem = { @@ -71,6 +73,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, + isSelected: option === fieldValue, })), }); } else { @@ -84,6 +87,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: selectedValue, searchText: selectedValue, tooltipText: selectedValue, + isSelected: true, }, ], }); @@ -130,27 +134,18 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, {({insets}) => ( <> - ) => - onSubmit({ - [fieldKey]: fieldValue === option.text ? '' : option.text, - }) - } + sectionTitleStyles={styles.mt5} + textInputValue={searchValue} + onSelectRow={(option) => onSubmit({[fieldKey]: fieldValue === option.text ? '' : option.text})} onChangeText={setSearchValue} - highlightSelectedOptions isRowMultilineSupported headerMessage={headerMessage} + initiallyFocusedOptionKey={fieldValue} /> )} diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 6f077f764474..631d377e34cd 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -7,9 +7,10 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {MagnifyingGlass} from '@components/Icon/Expensicons'; import OptionRow from '@components/OptionRow'; -import OptionsSelector from '@components/OptionsSelector'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; @@ -57,7 +58,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const [selectedOption, setSelectedOption] = useState(); const [searchTerm, setSearchTerm] = useState(''); const {inputCallbackRef} = useAutoFocusInput(); const {translate} = useLocalize(); @@ -105,11 +105,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const {policyID} = option; - if (policyID) { - setSelectedOption(option); - } else { - setSelectedOption(undefined); - } setActiveWorkspaceID(policyID); Navigation.goBack(); if (policyID !== activeWorkspaceID) { @@ -141,8 +136,9 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { boldStyle: hasUnreadData(policy?.id), keyForList: policy?.id, isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), + isSelected: policy?.id === activeWorkspaceID, })); - }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline]); + }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline, activeWorkspaceID]); const filteredAndSortedUserWorkspaces = useMemo( () => @@ -236,28 +232,20 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { {usersWorkspaces.length > 0 ? ( - = CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH} + textInputValue={searchTerm} onChangeText={setSearchTerm} - selectedOptions={selectedOption ? [selectedOption] : []} onSelectRow={selectPolicy} shouldPreventDefaultFocusOnSelectRow headerMessage={headerMessage} - highlightSelectedOptions - shouldShowOptions - autoFocus={false} - canSelectMultipleOptions={false} - shouldShowSubscript={false} - showTitleTooltip={false} - contentContainerStyles={[styles.pt0, styles.mt0]} - textIconLeft={MagnifyingGlass} - // Null is to avoid selecting unfocused option when Global selected, undefined is to focus selected workspace - initiallyFocusedOptionKey={!activeWorkspaceID ? null : undefined} + containerStyle={[styles.pt0, styles.mt0]} + textInputIconLeft={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? MagnifyingGlass : undefined} + initiallyFocusedOptionKey={activeWorkspaceID} + textInputAutoFocus={false} /> ) : ( @@ -269,7 +257,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { setSearchTerm, searchTerm, selectPolicy, - selectedOption, styles, theme.textSupporting, translate, @@ -281,14 +268,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { ], ); - useEffect(() => { - if (!activeWorkspaceID) { - return; - } - const optionToSet = usersWorkspaces.find((option) => option.policyID === activeWorkspaceID); - setSelectedOption(optionToSet); - }, [activeWorkspaceID, usersWorkspaces]); - return ( { + const attachLogToReport = (option: ListItem) => { if (!option.reportID) { return; } @@ -110,30 +111,24 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { testID={BaseShareLogList.displayName} includeSafeAreaPaddingBottom={false} > - {({safeAreaPaddingBottomStyle}) => ( - <> - Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} - /> - - - - - )} + Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} + /> + + + ); } diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index d590236e5256..8fb7d4f23691 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1130,6 +1130,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, { text: 'HR', @@ -1137,6 +1138,7 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, + isSelected: false, }, { text: 'Medical', @@ -1144,6 +1146,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, + isSelected: false, }, ], }, @@ -1159,6 +1162,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, ], }, @@ -1227,6 +1231,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, + isSelected: true, }, ], }, @@ -1240,6 +1245,7 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, + isSelected: false, }, ], }, @@ -1254,6 +1260,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, { text: 'Benefits', @@ -1261,6 +1268,7 @@ describe('OptionsListUtils', () => { searchText: 'Benefits', tooltipText: 'Benefits', isDisabled: false, + isSelected: false, }, { text: 'Cleaning', @@ -1268,6 +1276,7 @@ describe('OptionsListUtils', () => { searchText: 'Cleaning', tooltipText: 'Cleaning', isDisabled: false, + isSelected: false, }, { text: 'Food', @@ -1275,6 +1284,7 @@ describe('OptionsListUtils', () => { searchText: 'Food', tooltipText: 'Food', isDisabled: false, + isSelected: false, }, { text: 'HR', @@ -1282,6 +1292,7 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, + isSelected: false, }, { text: 'Software', @@ -1289,6 +1300,7 @@ describe('OptionsListUtils', () => { searchText: 'Software', tooltipText: 'Software', isDisabled: false, + isSelected: false, }, { text: 'Taxes', @@ -1296,6 +1308,7 @@ describe('OptionsListUtils', () => { searchText: 'Taxes', tooltipText: 'Taxes', isDisabled: false, + isSelected: false, }, ], }, From 826bf95f4fbe7327daeeb435f92536c26f7dd517 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 5 Apr 2024 15:10:50 +0200 Subject: [PATCH 41/46] Fix tests --- tests/unit/OptionsListUtilsTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 8fb7d4f23691..981f5285c88d 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1324,6 +1324,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, { text: 'Cleaning', @@ -1331,6 +1332,7 @@ describe('OptionsListUtils', () => { searchText: 'Cleaning', tooltipText: 'Cleaning', isDisabled: false, + isSelected: false, }, ], }, From 2e2d426dc612c24ffeff46ad74044c8c0dbd79c4 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 5 Apr 2024 15:34:53 +0200 Subject: [PATCH 42/46] Revert EditReportFieldDropdownPage --- src/pages/EditReportFieldDropdownPage.tsx | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 75c2a9c5be26..e887860ae155 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -2,9 +2,8 @@ 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 OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -43,7 +42,6 @@ type ReportFieldDropdownData = { keyForList: string; searchText: string; tooltipText: string; - isSelected?: boolean; }; type ReportFieldDropdownSectionItem = { @@ -73,7 +71,6 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, - isSelected: option === fieldValue, })), }); } else { @@ -87,7 +84,6 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: selectedValue, searchText: selectedValue, tooltipText: selectedValue, - isSelected: true, }, ], }); @@ -134,18 +130,27 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, {({insets}) => ( <> - onSubmit({[fieldKey]: fieldValue === option.text ? '' : option.text})} + // Focus the first option when searching + focusedIndex={0} + value={searchValue} + onSelectRow={(option: Record) => + onSubmit({ + [fieldKey]: fieldValue === option.text ? '' : option.text, + }) + } onChangeText={setSearchValue} + highlightSelectedOptions isRowMultilineSupported headerMessage={headerMessage} - initiallyFocusedOptionKey={fieldValue} /> )} From 1460677b82e832bfd8ac01317016acd6ca484c95 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 6 Apr 2024 00:52:29 +0800 Subject: [PATCH 43/46] add selector --- .../AppNavigator/createCustomBottomTabNavigator/TopBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 84d427389920..30d240027ecf 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -21,7 +21,7 @@ import type {Policy, Session as SessionType} from '@src/types/onyx'; type TopBarOnyxProps = { policy: OnyxEntry; - session: OnyxEntry; + session: OnyxEntry>; }; // eslint-disable-next-line react/no-unused-prop-types @@ -88,5 +88,6 @@ export default withOnyx({ }, session: { key: ONYXKEYS.SESSION, + selector: (session) => session && {authTokenType: session.authTokenType}, }, })(TopBar); From 18bc7b40ea975d4ba0f9d3c12ac2f4b36b50b4f9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 6 Apr 2024 02:12:37 +0800 Subject: [PATCH 44/46] DRY-ing code --- .../AppNavigator/createCustomBottomTabNavigator/TopBar.tsx | 2 +- src/libs/actions/Session/index.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 30d240027ecf..fd5282a8cfcd 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -31,7 +31,7 @@ function TopBar({policy, session}: TopBarProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; + const isAnonymousUser = Session.isAnonymousUser(session); const headerBreadcrumb = policy?.name ? {type: CONST.BREADCRUMB_TYPE.STRONG, text: policy.name} diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 17004baef43e..7f7531a094fa 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -2,7 +2,7 @@ import throttle from 'lodash/throttle'; import type {ChannelAuthorizationData} from 'pusher-js/types/src/core/auth/options'; import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import {InteractionManager, Linking, NativeModules} from 'react-native'; -import type {OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; @@ -175,8 +175,8 @@ function signOut() { /** * Checks if the account is an anonymous account. */ -function isAnonymousUser(): boolean { - return session.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; +function isAnonymousUser(sessionParam?: OnyxEntry): boolean { + return (sessionParam?.authTokenType ?? session.authTokenType) === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; } function hasStashedSession(): boolean { From e2b98a311375cd8ef9c1e85d060bf5c2a72c2ea7 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 5 Apr 2024 21:08:54 +0200 Subject: [PATCH 45/46] CR fix --- src/components/TagPicker/index.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index cd462dbe3f2e..b9d2d61efa7d 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -1,11 +1,9 @@ import React, {useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {EdgeInsets} from 'react-native-safe-area-context'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -42,12 +40,6 @@ type TagPickerProps = TagPickerOnyxProps & { /** Callback to submit the selected tag */ onSubmit: () => void; - /** - * Safe area insets required for reflecting the portion of the view, - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - */ - insets: EdgeInsets; - /** Should show the selected option that is disabled? */ shouldShowDisabledAndSelectedOption?: boolean; @@ -55,9 +47,8 @@ type TagPickerProps = TagPickerOnyxProps & { tagListIndex: number; }; -function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { +function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, onSubmit}: TagPickerProps) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -103,7 +94,6 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe return ( Date: Mon, 8 Apr 2024 08:53:41 +0200 Subject: [PATCH 46/46] CR fixes --- src/pages/EditRequestTagPage.js | 31 ++++++++----------- .../iou/request/step/IOURequestStepTag.js | 21 +++++-------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index 1aead9ee1f6e..fd9064f8b6fc 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -43,24 +43,19 @@ function EditRequestTagPage({defaultTag, policyID, tagListName, tagListIndex, on shouldEnableMaxHeight testID={EditRequestTagPage.displayName} > - {({insets}) => ( - <> - - {translate('iou.tagSelection')} - - - )} + + {translate('iou.tagSelection')} + ); } diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index ed55628ecaa9..3693e1cf9449 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -142,19 +142,14 @@ function IOURequestStepTag({ testID={IOURequestStepTag.displayName} shouldShowNotFoundPage={shouldShowNotFoundPage} > - {({insets}) => ( - <> - {translate('iou.tagSelection')} - - - )} + {translate('iou.tagSelection')} + ); }