From 1457b8b34ab9667d813da0da1a933e917bff57bd Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 1 Feb 2024 23:27:24 +0800 Subject: [PATCH 001/127] fix emoji picker keyboard issue --- src/CONST.ts | 9 + src/components/EmojiPicker/EmojiPicker.js | 1 + src/components/Modal/BaseModal.tsx | 35 ++- src/components/Modal/ModalContent.tsx | 19 ++ src/components/Modal/index.android.tsx | 10 - src/components/Modal/types.ts | 9 + src/libs/ComposerFocusManager.ts | 273 +++++++++++++++++- src/libs/focusComposerWithDelay.ts | 15 +- .../isWindowReadyToFocus/index.android.ts | 27 ++ src/libs/isWindowReadyToFocus/index.ts | 3 + 10 files changed, 363 insertions(+), 38 deletions(-) create mode 100644 src/components/Modal/ModalContent.tsx create mode 100644 src/libs/isWindowReadyToFocus/index.android.ts create mode 100644 src/libs/isWindowReadyToFocus/index.ts diff --git a/src/CONST.ts b/src/CONST.ts index 1ccdfd9a82a8..4c5523790a91 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -718,6 +718,15 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, + BUSINESS_TYPE: { + DEFAULT: 'default', + ATTACHMENT: 'attachment', + }, + RESTORE_FOCUS_TYPE: { + DEFAULT: 'default', + DELETE: 'delete', + PRESERVE: 'preserve', + }, }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index eaf89b7f64ea..10d040301e94 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -187,6 +187,7 @@ const EmojiPicker = forwardRef((props, ref) => { outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} avoidKeyboard + shouldEnableNewFocusManagement > , ) { @@ -55,6 +58,14 @@ function BaseModal( const isVisibleRef = useRef(isVisible); const wasVisible = usePrevious(isVisible); + const modalId = useMemo(() => ComposerFocusManager.getId(), []); + const saveFocusState = () => { + if (shouldEnableNewFocusManagement) { + ComposerFocusManager.saveFocusState(modalId); + } + ComposerFocusManager.resetReadyToFocus(modalId); + }; + /** * Hides modal * @param callHideCallback - Should we call the onModalHide callback @@ -69,11 +80,9 @@ function BaseModal( onModalHide(); } Modal.onModalDidClose(); - if (!fullscreen) { - ComposerFocusManager.setReadyToFocus(); - } + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusType); }, - [shouldSetModalVisibility, onModalHide, fullscreen], + [shouldSetModalVisibility, onModalHide, restoreFocusType, modalId], ); useEffect(() => { @@ -121,7 +130,7 @@ function BaseModal( }; const handleDismissModal = () => { - ComposerFocusManager.setReadyToFocus(); + ComposerFocusManager.setReadyToFocus(modalId); }; const { @@ -183,7 +192,7 @@ function BaseModal( onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} - onModalWillShow={() => ComposerFocusManager.resetReadyToFocus()} + onModalWillShow={saveFocusState} onDismiss={handleDismissModal} onSwipeComplete={() => onClose?.()} swipeDirection={swipeDirection} @@ -207,12 +216,14 @@ function BaseModal( avoidKeyboard={avoidKeyboard} customBackdrop={shouldUseCustomBackdrop ? : undefined} > - - {children} - + + + {children} + + ); } diff --git a/src/components/Modal/ModalContent.tsx b/src/components/Modal/ModalContent.tsx new file mode 100644 index 000000000000..5c8e0d2ece6b --- /dev/null +++ b/src/components/Modal/ModalContent.tsx @@ -0,0 +1,19 @@ +import type {ReactNode} from 'react'; +import React from 'react'; + +type ModalContentProps = { + /** Modal contents */ + children: ReactNode; + + /** called after modal content is dismissed */ + onDismiss: () => void; +}; + +function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect(() => () => onDismiss?.(), []); + return children; +} +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 86a1fd272185..7cb2c6083752 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,17 +1,7 @@ import React from 'react'; -import {AppState} from 'react-native'; -import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; import type BaseModalProps from './types'; -AppState.addEventListener('focus', () => { - ComposerFocusManager.setReadyToFocus(); -}); - -AppState.addEventListener('blur', () => { - ComposerFocusManager.resetReadyToFocus(); -}); - // Only want to use useNativeDriver on Android. It has strange flashes issue on IOS // https://github.com/react-native-modal/react-native-modal#the-modal-flashes-in-a-weird-way-when-animating function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index a0cdb737d448..43a2c281415a 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,6 +62,15 @@ type BaseModalProps = Partial & { /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */ shouldUseCustomBackdrop?: boolean; + + /** + * Whether the modal should enable the new focus manager. + * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. + * */ + shouldEnableNewFocusManagement?: boolean; + + /** How to re-focus after the modal is dismissed */ + restoreFocusType?: ValueOf; }; export default BaseModalProps; diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index b66bbe92599e..88e701a2e569 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -1,25 +1,278 @@ -let isReadyToFocusPromise = Promise.resolve(); -let resolveIsReadyToFocus: (value: void | PromiseLike) => void; +import type {View} from 'react-native'; +import {TextInput} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; -function resetReadyToFocus() { - isReadyToFocusPromise = new Promise((resolve) => { - resolveIsReadyToFocus = resolve; +type ModalId = number | undefined; + +type InputElement = (TextInput & HTMLElement) | null; + +/** + * So far, modern browsers only support the file cancel event in some newer versions + * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. + * We will introduce this prop to isolate the impact of the file upload modal on the focus stack. + */ +type BusinessType = ValueOf | undefined; + +type RestoreFocusType = ValueOf | undefined; + +type ModalContainer = View | HTMLElement | undefined | null; + +type FocusMapValue = { + input: InputElement; + businessType?: BusinessType; +}; + +type PromiseMapValue = { + ready: Promise; + resolve: () => void; +}; + +let focusedInput: InputElement = null; +let uniqueModalId = 1; +const focusMap = new Map(); +const activeModals: ModalId[] = []; +const promiseMap = new Map(); + +/** + * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible. + */ +function getActiveInput() { + return (TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField()) as InputElement; +} + +/** + * On web platform, if the modal is displayed by a click, the blur event is fired before the modal appears, + * so we need to cache the focused input in the pointerdown handler, which is fired before the blur event. + */ +function saveFocusedInput() { + focusedInput = getActiveInput(); +} + +/** + * If a click does not display the modal, we also should clear the cached value to avoid potential issues. + */ +function clearFocusedInput() { + if (!focusedInput) { + return; + } + + // we have to use timeout because of measureLayout + setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); +} + +/** + * When a TextInput is unmounted, we also should release the reference here to avoid potential issues. + * + */ +function releaseInput(input: InputElement) { + if (!input) { + return; + } + if (input === focusedInput) { + focusedInput = null; + } + [...focusMap].forEach(([key, value]) => { + if (value.input !== input) { + return; + } + focusMap.delete(key); + }); +} + +function getId() { + return uniqueModalId++; +} + +/** + * Save the focus state when opening the modal. + */ +function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container: ModalContainer = undefined) { + const activeInput = getActiveInput(); + + // For popoverWithoutOverlay, react calls autofocus before useEffect. + const input = focusedInput ?? activeInput; + focusedInput = null; + if (activeModals.indexOf(id) < 0) { + activeModals.push(id); + } + + if (shouldClearFocusWithType) { + [...focusMap].forEach(([key, value]) => { + if (value.businessType !== businessType) { + return; + } + focusMap.delete(key); + }); + } + + if (container && 'contains' in container && container.contains(input)) { + return; + } + focusMap.set(id, {input, businessType}); + if (!input) { + return; + } + input.blur(); +} + +/** + * On web platform, if we intentionally click on another input box, there is no need to restore focus. + * Additionally, if we are closing the RHP, we can ignore the focused input. + */ +function focus(input: InputElement, shouldIgnoreFocused = false) { + if (!input) { + return; + } + if (shouldIgnoreFocused) { + isWindowReadyToFocus().then(() => input.focus()); + return; + } + const activeInput = getActiveInput(); + if (activeInput) { + return; + } + isWindowReadyToFocus().then(() => input.focus()); +} + +/** + * Restore the focus state after the modal is dismissed. + */ +function restoreFocusState( + id: ModalId, + shouldIgnoreFocused = false, + restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, + businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, +) { + if (!id) { + return; + } + + // The stack is empty + if (activeModals.length < 1) { + return; + } + const index = activeModals.indexOf(id); + + // This id has been removed from the stack. + if (index < 0) { + return; + } + activeModals.splice(index, 1); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { + return; + } + + const {input} = focusMap.get(id) ?? {}; + focusMap.delete(id); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { + return; + } + + // This modal is not the topmost one, do not restore it. + if (activeModals.length > index) { + if (input) { + const lastId = activeModals.slice(-1)[0]; + focusMap.set(lastId, {...focusMap.get(lastId), input}); + } + return; + } + if (input) { + focus(input, shouldIgnoreFocused); + return; + } + + // Try to find the topmost one and restore it + const stack = [...focusMap].filter(([, v]) => v.input && v.businessType === businessType); + if (stack.length < 1) { + return; + } + const [lastId, {input: lastInput}] = stack.slice(-1)[0]; + + // The previous modal is still active + if (activeModals.indexOf(lastId) >= 0) { + return; + } + focus(lastInput, shouldIgnoreFocused); + focusMap.delete(lastId); +} + +function resetReadyToFocus(id: ModalId) { + const promise: PromiseMapValue = { + ready: Promise.resolve(), + resolve: () => {}, + }; + promise.ready = new Promise((resolve) => { + promise.resolve = resolve; }); + promiseMap.set(id, promise); +} + +/** + * Backward compatibility, for cases without an ID, it's fine to just take the topmost one. + */ +function getKey(id: ModalId) { + if (id) { + return id; + } + if (promiseMap.size < 1) { + return 0; + } + return [...promiseMap.keys()].slice(-1)[0]; } -function setReadyToFocus() { - if (!resolveIsReadyToFocus) { +function setReadyToFocus(id?: ModalId) { + const key = getKey(id); + const promise = promiseMap.get(key); + if (!promise) { return; } - resolveIsReadyToFocus(); + promise.resolve?.(); + promiseMap.delete(key); } -function isReadyToFocus(): Promise { - return isReadyToFocusPromise; +function isReadyToFocus(id?: ModalId) { + const key = getKey(id); + const promise = promiseMap.get(key); + if (!promise) { + return Promise.resolve(); + } + return promise.ready; +} + +function tryRestoreFocusAfterClosedCompletely(id: ModalId, restoreType: RestoreFocusType, businessType?: BusinessType) { + isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, businessType)); +} + +/** + * So far, this will only be called in file canceled event handler. + */ +function tryRestoreFocusByExternal(businessType: BusinessType) { + const stack = [...focusMap].filter(([, value]) => value.businessType === businessType && value.input); + if (stack.length < 1) { + return; + } + const [key, {input}] = stack.slice(-1)[0]; + focusMap.delete(key); + if (!input) { + return; + } + focus(input); } +export type {InputElement}; + export default { + getId, + saveFocusedInput, + clearFocusedInput, + releaseInput, + saveFocusState, + restoreFocusState, resetReadyToFocus, setReadyToFocus, isReadyToFocus, + tryRestoreFocusAfterClosedCompletely, + tryRestoreFocusByExternal, }; diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 6a2f85f7d311..a61c45325b3b 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,6 +1,7 @@ import type {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** @@ -22,12 +23,14 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD textInput.focus(); return; } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!textInput) { - return; - } - textInput.focus(); - }); + ComposerFocusManager.isReadyToFocus() + .then(isWindowReadyToFocus) + .then(() => { + if (!textInput) { + return; + } + textInput.focus(); + }); }; } diff --git a/src/libs/isWindowReadyToFocus/index.android.ts b/src/libs/isWindowReadyToFocus/index.android.ts new file mode 100644 index 000000000000..b9cca1b5a294 --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.android.ts @@ -0,0 +1,27 @@ +import {AppState} from 'react-native'; + +let isWindowReadyPromise = Promise.resolve(); +let resolveWindowReadyToFocus: () => void; + +AppState.addEventListener('focus', () => { + if (!resolveWindowReadyToFocus) { + return; + } + resolveWindowReadyToFocus(); +}); + +AppState.addEventListener('blur', () => { + isWindowReadyPromise = new Promise((resolve) => { + resolveWindowReadyToFocus = resolve; + }); +}); + +/** + * If we want to show the soft keyboard reliably, we need to ensure that the input's window gains focus first. + * Fortunately, we only need to manage the focus of the app window now, + * so we can achieve this by listening to the 'focus' event of the AppState. + * See {@link https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility#ShowReliably} + */ +const isWindowReadyToFocus = () => isWindowReadyPromise; + +export default isWindowReadyToFocus; diff --git a/src/libs/isWindowReadyToFocus/index.ts b/src/libs/isWindowReadyToFocus/index.ts new file mode 100644 index 000000000000..7ae3930c0c1d --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.ts @@ -0,0 +1,3 @@ +const isWindowReadyToFocus = () => Promise.resolve(); + +export default isWindowReadyToFocus; From 70e763c4652f0c29454b2658e3e096bea23b2ccf Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 2 Feb 2024 12:30:39 +0800 Subject: [PATCH 002/127] fix lint error --- src/components/Modal/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 43a2c281415a..6692f2751e40 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,7 +62,7 @@ type BaseModalProps = Partial & { /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */ shouldUseCustomBackdrop?: boolean; - + /** * Whether the modal should enable the new focus manager. * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. From 73d9a10a58b9fc5cda0dd6479f4acf635721f965 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 7 Feb 2024 20:48:26 +0500 Subject: [PATCH 003/127] 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 004/127] 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 005/127] 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 006/127] 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: Wed, 21 Feb 2024 00:26:00 +0800 Subject: [PATCH 007/127] polish code --- src/libs/ComposerFocusManager.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 88e701a2e569..4bc3edf91569 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -73,7 +73,7 @@ function releaseInput(input: InputElement) { if (input === focusedInput) { focusedInput = null; } - [...focusMap].forEach(([key, value]) => { + focusMap.forEach((value, key) => { if (value.input !== input) { return; } @@ -99,7 +99,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU } if (shouldClearFocusWithType) { - [...focusMap].forEach(([key, value]) => { + focusMap.forEach((value, key) => { if (value.businessType !== businessType) { return; } @@ -145,12 +145,7 @@ function restoreFocusState( restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, ) { - if (!id) { - return; - } - - // The stack is empty - if (activeModals.length < 1) { + if (!id || !activeModals.length) { return; } const index = activeModals.indexOf(id); @@ -219,7 +214,7 @@ function getKey(id: ModalId) { if (promiseMap.size < 1) { return 0; } - return [...promiseMap.keys()].slice(-1)[0]; + return [...promiseMap.keys()].at(-1); } function setReadyToFocus(id?: ModalId) { From 024bd2b69e6a1497a1e5f43d748397e76d9aca13 Mon Sep 17 00:00:00 2001 From: wentao Date: Fri, 23 Feb 2024 13:43:53 +0800 Subject: [PATCH 008/127] Update src/libs/ComposerFocusManager.ts Co-authored-by: Getabalew Tesfaye <75031127+getusha@users.noreply.github.com> --- src/libs/ComposerFocusManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 4bc3edf91569..7ee823475e93 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -36,7 +36,8 @@ const activeModals: ModalId[] = []; const promiseMap = new Map(); /** - * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible. + * Returns the ref of the currently focused text field, if one exists + * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible by using `currentlyFocusedField` instead. */ function getActiveInput() { return (TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField()) as InputElement; From e85ca868c0463eced93d11ebed7c20d77daf971e Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 23 Feb 2024 16:55:11 +0800 Subject: [PATCH 009/127] small improvements --- src/components/Modal/BaseModal.tsx | 2 +- src/components/Modal/ModalContent.tsx | 6 +++++- src/libs/ComposerFocusManager.ts | 14 ++++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index a2478d29253b..e9fa8ce46224 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -80,7 +80,7 @@ function BaseModal( onModalHide(); } Modal.onModalDidClose(); - ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusType); + ComposerFocusManager.refocusAfterModalFullyClosed(modalId, restoreFocusType); }, [shouldSetModalVisibility, onModalHide, restoreFocusType, modalId], ); diff --git a/src/components/Modal/ModalContent.tsx b/src/components/Modal/ModalContent.tsx index 5c8e0d2ece6b..49d3b049220f 100644 --- a/src/components/Modal/ModalContent.tsx +++ b/src/components/Modal/ModalContent.tsx @@ -5,7 +5,11 @@ type ModalContentProps = { /** Modal contents */ children: ReactNode; - /** called after modal content is dismissed */ + /** + * Callback method fired after modal content is unmounted. + * isVisible is not enough to cover all modal close cases, + * such as closing the attachment modal through the browser's back button. + * */ onDismiss: () => void; }; diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 7ee823475e93..a158f752d292 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -17,7 +17,7 @@ type BusinessType = ValueOf | undefined; type RestoreFocusType = ValueOf | undefined; -type ModalContainer = View | HTMLElement | undefined | null; +type ModalContainer = View & HTMLElement | undefined | null; type FocusMapValue = { input: InputElement; @@ -36,7 +36,7 @@ const activeModals: ModalId[] = []; const promiseMap = new Map(); /** - * Returns the ref of the currently focused text field, if one exists + * Returns the ref of the currently focused text field, if one exists. * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible by using `currentlyFocusedField` instead. */ function getActiveInput() { @@ -59,7 +59,9 @@ function clearFocusedInput() { return; } - // we have to use timeout because of measureLayout + // For the PopoverWithMeasuredContent component, Modal is only mounted after onLayout event is triggered, + // this event is placed within a setTimeout in react-native-web, + // so we can safely clear the cached value only after this event. setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); } @@ -108,7 +110,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU }); } - if (container && 'contains' in container && container.contains(input)) { + if (container?.contains(input)) { return; } focusMap.set(id, {input, businessType}); @@ -237,7 +239,7 @@ function isReadyToFocus(id?: ModalId) { return promise.ready; } -function tryRestoreFocusAfterClosedCompletely(id: ModalId, restoreType: RestoreFocusType, businessType?: BusinessType) { +function refocusAfterModalFullyClosed(id: ModalId, restoreType: RestoreFocusType, businessType?: BusinessType) { isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, businessType)); } @@ -269,6 +271,6 @@ export default { resetReadyToFocus, setReadyToFocus, isReadyToFocus, - tryRestoreFocusAfterClosedCompletely, + refocusAfterModalFullyClosed, tryRestoreFocusByExternal, }; From 3754145825af252edcf5a44fa1f9e17e467577a0 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 23 Feb 2024 17:17:48 +0800 Subject: [PATCH 010/127] optional improvement --- src/libs/ComposerFocusManager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index a158f752d292..84a54495aab9 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -114,10 +114,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU return; } focusMap.set(id, {input, businessType}); - if (!input) { - return; - } - input.blur(); + input?.blur(); } /** @@ -253,9 +250,6 @@ function tryRestoreFocusByExternal(businessType: BusinessType) { } const [key, {input}] = stack.slice(-1)[0]; focusMap.delete(key); - if (!input) { - return; - } focus(input); } From 251e467f19da7df069e69e11360fadd2b7b18f38 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 23 Feb 2024 17:32:48 +0800 Subject: [PATCH 011/127] function improvement --- src/libs/ComposerFocusManager.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 84a54495aab9..bfbf46887390 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -205,12 +205,9 @@ function resetReadyToFocus(id: ModalId) { } /** - * Backward compatibility, for cases without an ID, it's fine to just take the topmost one. + * Backward compatibility, for cases without an ModalId param, it's fine to just take the topmost one. */ -function getKey(id: ModalId) { - if (id) { - return id; - } +function getTopmostModalId() { if (promiseMap.size < 1) { return 0; } @@ -218,7 +215,7 @@ function getKey(id: ModalId) { } function setReadyToFocus(id?: ModalId) { - const key = getKey(id); + const key = id ?? getTopmostModalId(); const promise = promiseMap.get(key); if (!promise) { return; @@ -228,7 +225,7 @@ function setReadyToFocus(id?: ModalId) { } function isReadyToFocus(id?: ModalId) { - const key = getKey(id); + const key = id ?? getTopmostModalId(); const promise = promiseMap.get(key); if (!promise) { return Promise.resolve(); From fd7367769343b784a9370a6962e3f9c213329e34 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 23 Feb 2024 18:01:50 +0800 Subject: [PATCH 012/127] upload field improvement --- src/CONST.ts | 4 ---- src/libs/ComposerFocusManager.ts | 32 +++++++++++++++----------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 95e3312838d2..21c359b96e13 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -720,10 +720,6 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, - BUSINESS_TYPE: { - DEFAULT: 'default', - ATTACHMENT: 'attachment', - }, RESTORE_FOCUS_TYPE: { DEFAULT: 'default', DELETE: 'delete', diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index bfbf46887390..a4100e5172f5 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -8,20 +8,18 @@ type ModalId = number | undefined; type InputElement = (TextInput & HTMLElement) | null; -/** - * So far, modern browsers only support the file cancel event in some newer versions - * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. - * We will introduce this prop to isolate the impact of the file upload modal on the focus stack. - */ -type BusinessType = ValueOf | undefined; - type RestoreFocusType = ValueOf | undefined; type ModalContainer = View & HTMLElement | undefined | null; +/** + * So far, modern browsers only support the file cancel event in some newer versions + * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. + * We will introduce the isInUploadingContext field to isolate the impact of the upload modal on the other modals. + */ type FocusMapValue = { input: InputElement; - businessType?: BusinessType; + isInUploadingContext?: boolean; }; type PromiseMapValue = { @@ -91,7 +89,7 @@ function getId() { /** * Save the focus state when opening the modal. */ -function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container: ModalContainer = undefined) { +function saveFocusState(id: ModalId, isInUploadingContext = false, shouldClearFocusWithType = false, container: ModalContainer = undefined) { const activeInput = getActiveInput(); // For popoverWithoutOverlay, react calls autofocus before useEffect. @@ -103,7 +101,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU if (shouldClearFocusWithType) { focusMap.forEach((value, key) => { - if (value.businessType !== businessType) { + if (value.isInUploadingContext !== isInUploadingContext) { return; } focusMap.delete(key); @@ -113,7 +111,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU if (container?.contains(input)) { return; } - focusMap.set(id, {input, businessType}); + focusMap.set(id, {input, isInUploadingContext}); input?.blur(); } @@ -143,7 +141,7 @@ function restoreFocusState( id: ModalId, shouldIgnoreFocused = false, restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, - businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, + isInUploadingContext = false, ) { if (!id || !activeModals.length) { return; @@ -179,7 +177,7 @@ function restoreFocusState( } // Try to find the topmost one and restore it - const stack = [...focusMap].filter(([, v]) => v.input && v.businessType === businessType); + const stack = [...focusMap].filter(([, v]) => v.input && v.isInUploadingContext === isInUploadingContext); if (stack.length < 1) { return; } @@ -233,15 +231,15 @@ function isReadyToFocus(id?: ModalId) { return promise.ready; } -function refocusAfterModalFullyClosed(id: ModalId, restoreType: RestoreFocusType, businessType?: BusinessType) { - isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, businessType)); +function refocusAfterModalFullyClosed(id: ModalId, restoreType: RestoreFocusType, isInUploadingContext?: boolean) { + isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, isInUploadingContext)); } /** * So far, this will only be called in file canceled event handler. */ -function tryRestoreFocusByExternal(businessType: BusinessType) { - const stack = [...focusMap].filter(([, value]) => value.businessType === businessType && value.input); +function tryRestoreFocusByExternal(isInUploadingContext = false) { + const stack = [...focusMap].filter(([, value]) => value.isInUploadingContext === isInUploadingContext && value.input); if (stack.length < 1) { return; } From bc482fb313a8f21343f0578029eeda86c0a51fcd Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 23 Feb 2024 18:15:14 +0800 Subject: [PATCH 013/127] fix lint error --- src/libs/ComposerFocusManager.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index a4100e5172f5..19e9ea66e01e 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -10,7 +10,7 @@ type InputElement = (TextInput & HTMLElement) | null; type RestoreFocusType = ValueOf | undefined; -type ModalContainer = View & HTMLElement | undefined | null; +type ModalContainer = (View & HTMLElement) | undefined | null; /** * So far, modern browsers only support the file cancel event in some newer versions @@ -137,12 +137,7 @@ function focus(input: InputElement, shouldIgnoreFocused = false) { /** * Restore the focus state after the modal is dismissed. */ -function restoreFocusState( - id: ModalId, - shouldIgnoreFocused = false, - restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, - isInUploadingContext = false, -) { +function restoreFocusState(id: ModalId, shouldIgnoreFocused = false, restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, isInUploadingContext = false) { if (!id || !activeModals.length) { return; } From 5e7121a365e8a9b66a46e551ced9b30410ebc124 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 23 Feb 2024 21:23:55 +0300 Subject: [PATCH 014/127] fix auto focus on reply in thread --- src/components/Composer/index.tsx | 19 ++++++++----- src/libs/focusComposerWithDelay.ts | 28 +++++++++++-------- .../ComposerWithSuggestions.js | 3 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 516de55c73ba..8f9f457c7b51 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -73,6 +73,7 @@ function Composer( isReportActionCompose = false, isComposerFullSize = false, shouldContainScroll = false, + isMainComposer = false, ...props }: ComposerProps, ref: ForwardedRef, @@ -344,13 +345,17 @@ function Composer( disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!textInput.current) { - return; - } - - textInput.current.focus(); - }); + if (isMainComposer) { + ReportActionComposeFocusManager.onComposerFocus(null); + } else { + ReportActionComposeFocusManager.onComposerFocus(() => { + if (!textInput.current) { + return; + } + + textInput.current.focus(); + }); + } props.onFocus?.(e); }} diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 6a2f85f7d311..4d815db99c98 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,12 +1,14 @@ +import {InteractionManager} from 'react-native'; import type {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; +import ReportActionComposeFocusManager from './ReportActionComposeFocusManager'; type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** * Create a function that focuses the composer. */ -function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithDelay { +function focusComposerWithDelay(textInputRef: TextInput | null | undefined): FocusComposerWithDelay { /** * Focus the text input * @param [shouldDelay] Impose delay before focusing the text input @@ -14,19 +16,23 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { - return; - } + InteractionManager.runAfterInteractions(() => { + const textInput = textInputRef ?? ReportActionComposeFocusManager.composerRef.current; - if (!shouldDelay) { - textInput.focus(); - return; - } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!textInput) { + if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; } - textInput.focus(); + + if (!shouldDelay) { + textInput.focus(); + return; + } + ComposerFocusManager.isReadyToFocus().then(() => { + if (!textInput) { + return; + } + textInput.focus(); + }); }); }; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 12e31495af2b..d611ddf591ab 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -447,7 +447,7 @@ function ComposerWithSuggestions({ * @memberof ReportActionCompose */ const focus = useCallback((shouldDelay = false) => { - focusComposerWithDelay(textInputRef.current)(shouldDelay); + focusComposerWithDelay()(shouldDelay); }, []); const setUpComposeFocusManager = useCallback(() => { @@ -592,6 +592,7 @@ function ComposerWithSuggestions({ <> Date: Sun, 25 Feb 2024 22:54:18 +0500 Subject: [PATCH 015/127] 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 3c62ad7a85ad37e8f24cf4fdbf3f9d8d3e439cb3 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 28 Feb 2024 09:14:37 +0700 Subject: [PATCH 016/127] fix tag list show required --- ...eyTemporaryForRefactorRequestConfirmationList.js | 13 ++++++++----- src/libs/Permissions.ts | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8eeaeaf87eff..3ad273492d99 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -267,6 +267,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + // Check if it is the multiple levels tags or not + const isMultipleLevelsTags = policyTagLists && policyTagLists.length > 1; + // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); @@ -762,7 +765,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ shouldShow: shouldShowCategories, isSupplementary: !isCategoryRequired, }, - ..._.map(policyTagLists, ({name}, index) => ({ + ..._.map(policyTagLists, ({name, required}, index) => ({ item: ( ), shouldShow: shouldShowTags, @@ -924,17 +927,17 @@ export default compose( key: ONYXKEYS.SESSION, }, policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${'BD2590A6E1619441'}`, }, policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${'BD2590A6E1619441'}`, }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${'BD2590A6E1619441'}`, }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index ce5e0e674c59..104b4218da9d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -23,6 +23,7 @@ function canUseReportFields(betas: OnyxEntry): boolean { } function canUseViolations(betas: OnyxEntry): boolean { + return true return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } From bce63913f98271051b213f0b277d06ec1e02ce6a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 28 Feb 2024 09:28:44 +0700 Subject: [PATCH 017/127] fix remove redundant code --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 6 +++--- src/libs/Permissions.ts | 1 - src/libs/isReportMessageAttachment.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 3ad273492d99..1153293126a6 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -927,17 +927,17 @@ export default compose( key: ONYXKEYS.SESSION, }, policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${'BD2590A6E1619441'}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, }, policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${'BD2590A6E1619441'}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${'BD2590A6E1619441'}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 104b4218da9d..ce5e0e674c59 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -23,7 +23,6 @@ function canUseReportFields(betas: OnyxEntry): boolean { } function canUseViolations(betas: OnyxEntry): boolean { - return true return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index fd03adcffd93..d25d841c1395 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -18,5 +18,5 @@ export default function isReportMessageAttachment({text, html, translationKey}: } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return (text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(text)) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return (text === CONST.ATTACHMENT_MESSAGE_TEXT) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } From cd91a49350555e25aebd1680b8cfed06d8558243 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 28 Feb 2024 09:33:27 +0700 Subject: [PATCH 018/127] fix remove redundant logic --- src/libs/isReportMessageAttachment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index d25d841c1395..fd03adcffd93 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -18,5 +18,5 @@ export default function isReportMessageAttachment({text, html, translationKey}: } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return (text === CONST.ATTACHMENT_MESSAGE_TEXT) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return (text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(text)) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } From 628c44d4a2139b5510a1cd2d997c843063b787eb Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 29 Feb 2024 22:00:59 +0800 Subject: [PATCH 019/127] fix switch bug --- src/components/EmojiPicker/EmojiPicker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index 6a963db22b8b..3ab0f398fa9d 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -192,6 +192,7 @@ const EmojiPicker = forwardRef((props, ref) => { innerContainerStyle={styles.popoverInnerContainer} avoidKeyboard shouldEnableNewFocusManagement + restoreFocusType={CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE} > Date: Mon, 4 Mar 2024 01:18:41 +0700 Subject: [PATCH 020/127] fix single tag case --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 202e2c04dc12..e4b4d8c20bb8 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -267,9 +267,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - // Check if it is the multiple levels tags or not - const isMultipleLevelsTags = policyTagLists && policyTagLists.length > 1; - // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); @@ -782,11 +779,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ style={[styles.moneyRequestMenuItem]} disabled={didConfirm} interactive={!isReadOnly} - rightLabel={(isMultipleLevelsTags ? required : isTagRequired) ? translate('common.required') : ''} + rightLabel={required ? translate('common.required') : ''} /> ), shouldShow: shouldShowTags, - isSupplementary: !isTagRequired, + isSupplementary: !required, })), { item: ( From cde60148a778f3402781388608db78a802e56a97 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 4 Mar 2024 01:34:12 +0700 Subject: [PATCH 021/127] fix lint --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index e4b4d8c20bb8..30eaf69ed2a7 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -310,7 +310,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; const isCategoryRequired = canUseViolations && lodashGet(policy, 'requiresCategory', false); - const isTagRequired = canUseViolations && lodashGet(policy, 'requiresTag', false); useEffect(() => { if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { From 247827bafdb8ce9fe5478e5858c6c222c242ea81 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 4 Mar 2024 01:59:38 +0700 Subject: [PATCH 022/127] fix create isTagRequired variable --- ...oraryForRefactorRequestConfirmationList.js | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 30eaf69ed2a7..bde6850425cb 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,5 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; +import {isTag} from 'domhandler'; +import {isUndefined} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; @@ -762,28 +764,38 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ shouldShow: shouldShowCategories, isSupplementary: !isCategoryRequired, }, - ..._.map(policyTagLists, ({name, required}, index) => ({ - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.CREATE, iouType, index, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={required ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowTags, - isSupplementary: !required, - })), + ..._.map(policyTagLists, ({name, required}, index) => { + const isTagRequired = !isUndefined(required) && required; + return { + item: ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( + CONST.IOU.ACTION.CREATE, + iouType, + index, + transaction.transactionID, + reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) + } + style={[styles.moneyRequestMenuItem]} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isTagRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowTags, + isSupplementary: !isTagRequired, + }; + }), { item: ( Date: Mon, 4 Mar 2024 02:04:58 +0700 Subject: [PATCH 023/127] fix remove isTag --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index bde6850425cb..96f7a3187d24 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,6 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; -import {isTag} from 'domhandler'; import {isUndefined} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; From f0c955bb6c9b13479de91e813ce73a7761ab0301 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 5 Mar 2024 09:24:17 +0700 Subject: [PATCH 024/127] fix add canUseViolation --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 96f7a3187d24..fb0e4884a841 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -764,7 +764,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isSupplementary: !isCategoryRequired, }, ..._.map(policyTagLists, ({name, required}, index) => { - const isTagRequired = !isUndefined(required) && required; + const isTagRequired = isUndefined(required) ? false : canUseViolations && required; return { item: ( Date: Wed, 6 Mar 2024 07:46:07 +0700 Subject: [PATCH 025/127] fix lint --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 43d2bb31dc84..2e4a1f5f937b 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; -import {isUndefined} from 'lodash'; import Str from 'expensify-common/lib/str'; +import {isUndefined} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; From b7f306e5b8f07c21279dc68352247de1f83b5314 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Thu, 7 Mar 2024 00:19:49 +0300 Subject: [PATCH 026/127] refactor on the implementation --- src/components/Composer/index.tsx | 6 +++-- src/libs/ReportActionComposeFocusManager.ts | 2 +- src/libs/focusComposerWithDelay.ts | 27 +++++++++---------- .../report/ContextMenu/ContextMenuActions.tsx | 5 +++- .../ComposerWithSuggestions.tsx | 3 +-- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 4baff0c07321..d1981e3c7f32 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -73,7 +73,6 @@ function Composer( isReportActionCompose = false, isComposerFullSize = false, shouldContainScroll = false, - isMainComposer = false, ...props }: ComposerProps, ref: ForwardedRef, @@ -346,9 +345,12 @@ function Composer( disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { - if (isMainComposer) { + if (isReportActionCompose) { ReportActionComposeFocusManager.onComposerFocus(null); } else { + // While a user was editing a comment and if they open on LHN menu we want the focus to return + // to the ReportActionItemMessageEdit compose after they click on the menu (for e.g. mark as read) + // so we assign the focus callback here. ReportActionComposeFocusManager.onComposerFocus(() => { if (!textInput.current) { return; diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 123d97987e14..64e4530e0fe5 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -18,7 +18,7 @@ let mainComposerFocusCallback: FocusCallback | null = null; * * @param callback callback to register */ -function onComposerFocus(callback: FocusCallback, isMainComposer = false) { +function onComposerFocus(callback: FocusCallback | null, isMainComposer = false) { if (isMainComposer) { mainComposerFocusCallback = callback; } else { diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 4d815db99c98..4909c4c0a4ea 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,4 +1,3 @@ -import {InteractionManager} from 'react-native'; import type {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; @@ -8,7 +7,7 @@ type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** * Create a function that focuses the composer. */ -function focusComposerWithDelay(textInputRef: TextInput | null | undefined): FocusComposerWithDelay { +function focusComposerWithDelay(textInputRef: TextInput | null): FocusComposerWithDelay { /** * Focus the text input * @param [shouldDelay] Impose delay before focusing the text input @@ -16,23 +15,21 @@ function focusComposerWithDelay(textInputRef: TextInput | null | undefined): Foc return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - InteractionManager.runAfterInteractions(() => { - const textInput = textInputRef ?? ReportActionComposeFocusManager.composerRef.current; + const textInput = textInputRef ?? ReportActionComposeFocusManager.composerRef.current; - if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { - return; - } + if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { + return; + } - if (!shouldDelay) { - textInput.focus(); + if (!shouldDelay) { + textInput.focus(); + return; + } + ComposerFocusManager.isReadyToFocus().then(() => { + if (!textInput) { return; } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!textInput) { - return; - } - textInput.focus(); - }); + textInput.focus(); }); }; } diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 831b32def2bb..1fd00a7f3813 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,6 +1,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {MutableRefObject} from 'react'; import React from 'react'; +import {InteractionManager} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; @@ -194,7 +195,9 @@ const ContextMenuActions: ContextMenuAction[] = [ onPress: (closePopover, {reportAction, reportID}) => { if (closePopover) { hideContextMenu(false, () => { - ReportActionComposeFocusManager.focus(); + InteractionManager.runAfterInteractions(() => { + ReportActionComposeFocusManager.focus(); + }); Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); }); return; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index b6b76a77f44c..4926bc6b7659 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -581,7 +581,7 @@ function ComposerWithSuggestions( * @param [shouldDelay=false] Impose delay before focusing the composer */ const focus = useCallback((shouldDelay = false) => { - focusComposerWithDelay()(shouldDelay); + focusComposerWithDelay(null)(shouldDelay); }, []); const setUpComposeFocusManager = useCallback(() => { @@ -725,7 +725,6 @@ function ComposerWithSuggestions( <> Date: Thu, 7 Mar 2024 22:33:32 +0300 Subject: [PATCH 027/127] pass text input ref --- src/libs/focusComposerWithDelay.ts | 3 +-- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 4909c4c0a4ea..958b13855008 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,7 +1,6 @@ import type {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; -import ReportActionComposeFocusManager from './ReportActionComposeFocusManager'; type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** @@ -15,7 +14,7 @@ function focusComposerWithDelay(textInputRef: TextInput | null): FocusComposerWi return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - const textInput = textInputRef ?? ReportActionComposeFocusManager.composerRef.current; + const textInput = textInputRef; if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 4926bc6b7659..af2d0b9eab56 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -581,7 +581,7 @@ function ComposerWithSuggestions( * @param [shouldDelay=false] Impose delay before focusing the composer */ const focus = useCallback((shouldDelay = false) => { - focusComposerWithDelay(null)(shouldDelay); + focusComposerWithDelay(textInputRef.current)(shouldDelay); }, []); const setUpComposeFocusManager = useCallback(() => { From 69e6022b79b61e8c82c6d98d7581ff2ec5e807f7 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Thu, 7 Mar 2024 22:43:34 +0300 Subject: [PATCH 028/127] revert ref variable change --- src/libs/focusComposerWithDelay.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 958b13855008..7ee342d0cce4 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -6,7 +6,7 @@ type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** * Create a function that focuses the composer. */ -function focusComposerWithDelay(textInputRef: TextInput | null): FocusComposerWithDelay { +function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithDelay { /** * Focus the text input * @param [shouldDelay] Impose delay before focusing the text input @@ -14,7 +14,6 @@ function focusComposerWithDelay(textInputRef: TextInput | null): FocusComposerWi return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - const textInput = textInputRef; if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; From 2371784e51b4d80bf2bf7e54d1bb5da2568647f3 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 8 Mar 2024 22:21:07 +0800 Subject: [PATCH 029/127] code style improvement --- src/libs/ComposerFocusManager.ts | 58 ++++++++++++-------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 19e9ea66e01e..04243454d541 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -120,18 +120,26 @@ function saveFocusState(id: ModalId, isInUploadingContext = false, shouldClearFo * Additionally, if we are closing the RHP, we can ignore the focused input. */ function focus(input: InputElement, shouldIgnoreFocused = false) { - if (!input) { + const activeInput = getActiveInput(); + if (!input || activeInput && !shouldIgnoreFocused) { return; } - if (shouldIgnoreFocused) { - isWindowReadyToFocus().then(() => input.focus()); + isWindowReadyToFocus().then(() => input.focus()); +} + +function tryRestoreTopmostFocus(shouldIgnoreFocused: boolean, isInUploadingContext = false) { + const topmost = [...focusMap].filter(([, v]) => v.input && v.isInUploadingContext === isInUploadingContext).at(-1); + if (topmost === undefined) { return; } - const activeInput = getActiveInput(); - if (activeInput) { + const [modalId, {input}] = topmost; + + // This modal is still active + if (activeModals.indexOf(modalId) >= 0) { return; } - isWindowReadyToFocus().then(() => input.focus()); + focus(input, shouldIgnoreFocused); + focusMap.delete(modalId); } /** @@ -141,13 +149,13 @@ function restoreFocusState(id: ModalId, shouldIgnoreFocused = false, restoreFocu if (!id || !activeModals.length) { return; } - const index = activeModals.indexOf(id); + const activeModalIndex = activeModals.indexOf(id); // This id has been removed from the stack. - if (index < 0) { + if (activeModalIndex < 0) { return; } - activeModals.splice(index, 1); + activeModals.splice(activeModalIndex, 1); if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { return; } @@ -159,9 +167,9 @@ function restoreFocusState(id: ModalId, shouldIgnoreFocused = false, restoreFocu } // This modal is not the topmost one, do not restore it. - if (activeModals.length > index) { + if (activeModals.length > activeModalIndex) { if (input) { - const lastId = activeModals.slice(-1)[0]; + const lastId = activeModals.at(-1); focusMap.set(lastId, {...focusMap.get(lastId), input}); } return; @@ -172,18 +180,7 @@ function restoreFocusState(id: ModalId, shouldIgnoreFocused = false, restoreFocu } // Try to find the topmost one and restore it - const stack = [...focusMap].filter(([, v]) => v.input && v.isInUploadingContext === isInUploadingContext); - if (stack.length < 1) { - return; - } - const [lastId, {input: lastInput}] = stack.slice(-1)[0]; - - // The previous modal is still active - if (activeModals.indexOf(lastId) >= 0) { - return; - } - focus(lastInput, shouldIgnoreFocused); - focusMap.delete(lastId); + tryRestoreTopmostFocus(shouldIgnoreFocused, isInUploadingContext); } function resetReadyToFocus(id: ModalId) { @@ -230,19 +227,6 @@ function refocusAfterModalFullyClosed(id: ModalId, restoreType: RestoreFocusType isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, isInUploadingContext)); } -/** - * So far, this will only be called in file canceled event handler. - */ -function tryRestoreFocusByExternal(isInUploadingContext = false) { - const stack = [...focusMap].filter(([, value]) => value.isInUploadingContext === isInUploadingContext && value.input); - if (stack.length < 1) { - return; - } - const [key, {input}] = stack.slice(-1)[0]; - focusMap.delete(key); - focus(input); -} - export type {InputElement}; export default { @@ -256,5 +240,5 @@ export default { setReadyToFocus, isReadyToFocus, refocusAfterModalFullyClosed, - tryRestoreFocusByExternal, + tryRestoreTopmostFocus, }; From 6fe725d3b4ac8fd8be2045b12497aec7ed208f67 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 8 Mar 2024 22:31:13 +0800 Subject: [PATCH 030/127] fix lint error --- src/libs/ComposerFocusManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 04243454d541..d793c202d243 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -121,7 +121,7 @@ function saveFocusState(id: ModalId, isInUploadingContext = false, shouldClearFo */ function focus(input: InputElement, shouldIgnoreFocused = false) { const activeInput = getActiveInput(); - if (!input || activeInput && !shouldIgnoreFocused) { + if (!input || (activeInput && !shouldIgnoreFocused)) { return; } isWindowReadyToFocus().then(() => input.focus()); From 78d815b9b4cc94f5a9a77327699f435051c5db31 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 20 Mar 2024 23:54:44 +0300 Subject: [PATCH 031/127] minor fix --- src/libs/focusComposerWithDelay.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 7ee342d0cce4..c8b0a390f89a 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -18,7 +18,6 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; } - if (!shouldDelay) { textInput.focus(); return; From 1b9163c70d17bd5d5286f8a1974fdd2c432534ae Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 20 Mar 2024 23:55:43 +0300 Subject: [PATCH 032/127] minor fix --- src/libs/focusComposerWithDelay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index c8b0a390f89a..d01dfd662e1a 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -14,7 +14,6 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; } @@ -22,6 +21,7 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD textInput.focus(); return; } + ComposerFocusManager.isReadyToFocus().then(() => { if (!textInput) { return; From aa4fe300293fe42d0eb13e0f68bebb6dc02c5a8f Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 20 Mar 2024 23:56:45 +0300 Subject: [PATCH 033/127] minor fix --- src/libs/focusComposerWithDelay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index d01dfd662e1a..c8b0a390f89a 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -14,6 +14,7 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. + if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; } @@ -21,7 +22,6 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD textInput.focus(); return; } - ComposerFocusManager.isReadyToFocus().then(() => { if (!textInput) { return; From 4c3483018962f4509294a616df823c5a6872c015 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 20 Mar 2024 23:59:24 +0300 Subject: [PATCH 034/127] minor fix --- src/libs/focusComposerWithDelay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index c8b0a390f89a..6a2f85f7d311 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -14,10 +14,10 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD return (shouldDelay = false) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. - if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { return; } + if (!shouldDelay) { textInput.focus(); return; From 177bd399efa0960749e6151573cf5609071ab93c Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 21 Mar 2024 14:04:44 +0000 Subject: [PATCH 035/127] update card typescript types --- src/types/onyx/Card.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 53415690705a..48f51a8759f7 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -8,13 +8,12 @@ type Card = { bank: string; availableSpend: number; domainName: string; - maskedPan?: string; // do not reference, removing as part of Expensify/App#27943 lastFourPAN?: string; - cardName: string; - cardTitle: string; // used only for virtual limit cards + cardDisplayName: string; limitType: ValueOf; isAdminIssuedVirtualCard: boolean; isVirtual: boolean; + isExpensifyCard: boolean; fraud: ValueOf; cardholderFirstName: string; cardholderLastName: string; From a641c83f49a1f2cad18df04a02b930ee537abf74 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 18:56:48 +0000 Subject: [PATCH 036/127] nameValuePairs --- src/types/onyx/Card.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 48f51a8759f7..c5cc467e12af 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -9,16 +9,17 @@ type Card = { availableSpend: number; domainName: string; lastFourPAN?: string; - cardDisplayName: string; - limitType: ValueOf; - isAdminIssuedVirtualCard: boolean; isVirtual: boolean; - isExpensifyCard: boolean; fraud: ValueOf; cardholderFirstName: string; cardholderLastName: string; errors?: OnyxCommon.Errors; isLoading?: boolean; + nameValuePairs?: { + limitType?: ValueOf; + cardTitle?: string; // used only for virtual limit cards + issuedBy?: string; + } }; type TCardDetails = { From c7516ee5e63696ae6e04ca3dbacb2faf15959e67 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Fri, 22 Mar 2024 19:05:29 +0000 Subject: [PATCH 037/127] add other keys that may be returned --- src/types/onyx/Card.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index c5cc467e12af..34e8c92d7dc7 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -19,7 +19,13 @@ type Card = { limitType?: ValueOf; cardTitle?: string; // used only for virtual limit cards issuedBy?: string; - } + hasCustomUnapprovedExpenseLimit?: string; + unapprovedExpenseLimit?: string; + feedCountry?: string; + isVirtual?: string; + previousState?: string; + expirationDate?: string; + }; }; type TCardDetails = { From 35909dc7d62ea1b5869f813db0309ca52877306d Mon Sep 17 00:00:00 2001 From: Georgia Monahan <38015950+grgia@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:22:30 +0000 Subject: [PATCH 038/127] Fix Types Co-authored-by: Maria D'Costa --- src/types/onyx/Card.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 34e8c92d7dc7..c045d477b9aa 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -18,12 +18,12 @@ type Card = { nameValuePairs?: { limitType?: ValueOf; cardTitle?: string; // used only for virtual limit cards - issuedBy?: string; - hasCustomUnapprovedExpenseLimit?: string; - unapprovedExpenseLimit?: string; + issuedBy?: number; + hasCustomUnapprovedExpenseLimit?: boolean; + unapprovedExpenseLimit?: number; feedCountry?: string; - isVirtual?: string; - previousState?: string; + isVirtual?: boolean; + previousState?: number; expirationDate?: string; }; }; From cd8d6c2c950bfda4510de3fc3ad8b9e741870946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 25 Mar 2024 13:16:33 +0100 Subject: [PATCH 039/127] refactorings --- .../ShareLogList/BaseShareLogList.tsx | 114 +++++++----------- .../settings/AboutPage/ShareLogList/types.ts | 7 +- 2 files changed, 48 insertions(+), 73 deletions(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 1a7b23477349..aea358e43ce0 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -1,66 +1,62 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import React, {useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {usePersonalDetails} from '@components/OnyxProvider'; -import OptionsSelector from '@components/OptionsSelector'; +import {useBetas, usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -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 CONST from '@src/CONST'; 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) { - const [searchValue, setSearchValue] = useState(''); - const [searchOptions, setSearchOptions] = useState>({ - recentReports: [], - personalDetails: [], - userToInvite: null, - }); - +function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) { + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const styles = useThemeStyles(); const isMounted = useRef(false); const personalDetails = usePersonalDetails(); + const betas = useBetas(); + + const searchOptions = useMemo(() => { + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - const updateOptions = useCallback(() => { + if (!isMounted.current || !isOptionsDataReady) { + isMounted.current = true; + return { + recentReports: [], + personalDetails: [], + userToInvite: undefined, + headerMessage: '', + }; + } const { recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, - } = OptionsListUtils.getShareLogOptions(reports, personalDetails, searchValue.trim(), betas ?? []); + } = OptionsListUtils.getShareLogOptions(reports, personalDetails, debouncedSearchValue.trim(), betas ?? []); + + const header = OptionsListUtils.getHeaderMessage( + (searchOptions?.recentReports?.length || 0) + (searchOptions?.personalDetails?.length || 0) !== 0, + Boolean(searchOptions?.userToInvite), + debouncedSearchValue, + ); - setSearchOptions({ + return { recentReports: localRecentReports, personalDetails: localPersonalDetails, userToInvite: localUserToInvite, - }); - }, [betas, personalDetails, reports, searchValue]); - - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - - useEffect(() => { - updateOptions(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!isMounted.current) { - isMounted.current = true; - return; - } - - updateOptions(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchValue]); + headerMessage: header, + }; + }, [betas, personalDetails, reports, debouncedSearchValue]); const sections = useMemo(() => { const sectionsList = []; @@ -91,17 +87,7 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis } return sectionsList; - }, [searchOptions.personalDetails, searchOptions.recentReports, searchOptions.userToInvite, translate]); - - const headerMessage = OptionsListUtils.getHeaderMessage( - searchOptions.recentReports.length + searchOptions.personalDetails.length !== 0, - Boolean(searchOptions.userToInvite), - searchValue, - ); - - const onChangeText = (value = '') => { - setSearchValue(value); - }; + }, [searchOptions?.personalDetails, searchOptions?.recentReports, searchOptions?.userToInvite, translate]); const attachLogToReport = (option: Report) => { if (!option.reportID) { @@ -117,28 +103,24 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis testID={BaseShareLogList.displayName} includeSafeAreaPaddingBottom={false} > - {({safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( <> Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} /> - - - + )} @@ -151,8 +133,4 @@ export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, })(BaseShareLogList); diff --git a/src/pages/settings/AboutPage/ShareLogList/types.ts b/src/pages/settings/AboutPage/ShareLogList/types.ts index abbdbfb88e0b..6d385e6cac74 100644 --- a/src/pages/settings/AboutPage/ShareLogList/types.ts +++ b/src/pages/settings/AboutPage/ShareLogList/types.ts @@ -1,10 +1,7 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {Beta, Report} from '@src/types/onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {Report} from '@src/types/onyx'; type BaseShareLogListOnyxProps = { - /** Beta features list */ - betas: OnyxEntry; - /** All reports shared with the user */ reports: OnyxCollection; }; From 36bdea88dc89fcfdcc9491011ee7ec04934c4ec9 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Mon, 25 Mar 2024 14:11:45 +0000 Subject: [PATCH 040/127] remove unused keys --- src/types/onyx/Card.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index c045d477b9aa..d9df6513d89f 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -11,8 +11,6 @@ type Card = { lastFourPAN?: string; isVirtual: boolean; fraud: ValueOf; - cardholderFirstName: string; - cardholderLastName: string; errors?: OnyxCommon.Errors; isLoading?: boolean; nameValuePairs?: { From 52a9f5ea41746aa443fa42b0f425dc591df21f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 26 Mar 2024 11:24:41 +0100 Subject: [PATCH 041/127] removed isMounted flag --- .../settings/AboutPage/ShareLogList/BaseShareLogList.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index aea358e43ce0..3fdf503023c4 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef} from 'react'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useBetas, usePersonalDetails} from '@components/OnyxProvider'; @@ -22,15 +22,13 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const isMounted = useRef(false); const personalDetails = usePersonalDetails(); const betas = useBetas(); const searchOptions = useMemo(() => { const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - if (!isMounted.current || !isOptionsDataReady) { - isMounted.current = true; + if (!isOptionsDataReady) { return { recentReports: [], personalDetails: [], From daeb7218cbf0b6e11dfa893f33e635a142b73f41 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 26 Mar 2024 12:24:40 +0100 Subject: [PATCH 042/127] Replace OptionsSelector with SelectionList --- src/components/SelectionList/BaseListItem.tsx | 16 +++++ .../SelectionList/BaseSelectionList.tsx | 18 ++++-- src/components/SelectionList/types.ts | 18 +++++- src/components/TagPicker/index.js | 19 +++--- src/libs/OptionsListUtils.ts | 17 +++-- src/pages/EditReportFieldDropdownPage.tsx | 26 ++++---- src/pages/EditReportFieldPage.tsx | 2 +- src/pages/WorkspaceSwitcherPage.tsx | 64 +++++-------------- .../ShareLogList/BaseShareLogList.tsx | 21 +++--- 9 files changed, 102 insertions(+), 99 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 596951374099..95234dec7dde 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -130,6 +130,22 @@ function BaseListItem({ )} + {!item.isSelected && item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( + + + + )} + {!item.isSelected && item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO && ( + + + + )} {rightHandSideComponentRender()} {FooterComponent} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 015fd284c0b4..c31ee75c9cf7 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -69,6 +69,9 @@ function BaseSelectionList( listHeaderWrapperStyle, isRowMultilineSupported = false, textInputRef, + textInputIconLeft, + sectionTitleStyles, + turnOffEnterDisabling, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -77,7 +80,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(); @@ -192,7 +195,7 @@ function BaseSelectionList( ); // Disable `Enter` shortcut if the active element is a button or checkbox - const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); + const disableEnterShortcut = !turnOffEnterDisabling && activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); /** * Scrolls to the desired item index in the section list @@ -306,7 +309,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} ); @@ -492,8 +495,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} @@ -506,6 +513,7 @@ function BaseSelectionList( inputMode={inputMode} selectTextOnFocus spellCheck={false} + iconLeft={textInputIconLeft} onSubmitEditing={selectFocusedOption} blurOnSubmit={!!flattenedSections.allOptions.length} isLoading={isLoadingNewOptions} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index e691a5bdb191..55c8d48bc431 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -2,10 +2,12 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; import type {ValueOf} from 'type-fest'; 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 RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; @@ -110,6 +112,8 @@ type ListItem = { /** The search value from the selection list */ searchText?: string | null; + + brickRoadIndicator?: BrickRoad | '' | null; }; type ListItemProps = CommonListItemProps & { @@ -137,6 +141,7 @@ type BaseListItemProps = CommonListItemProps & { pendingAction?: PendingAction | null; FooterComponent?: ReactElement; children?: ReactElement | ((hovered: boolean) => ReactElement); + brickRoadIndicator?: BrickRoad | '' | null; }; type UserListItemProps = ListItemProps & { @@ -208,6 +213,9 @@ type BaseSelectionListProps = Partial & { /** Max length for the text input */ textInputMaxLength?: number; + /** Icon to display on the left side of TextInput */ + textInputIconLeft?: IconAsset | null; + /** Callback to fire when the text input changes */ onChangeText?: (text: string) => void; @@ -266,7 +274,7 @@ type BaseSelectionListProps = Partial & { disableInitialFocusOptionStyle?: boolean; /** Styles to apply to SelectionList container */ - containerStyle?: ViewStyle; + containerStyle?: StyleProp; /** Whether keyboard is visible on the screen */ isKeyboardShown?: boolean; @@ -299,7 +307,13 @@ type BaseSelectionListProps = Partial & { isRowMultilineSupported?: boolean; /** Ref for textInput */ - textInputRef?: MutableRefObject; + textInputRef?: MutableRefObject | ((ref: TextInput | null) => void); + + /** Styles for the section title */ + sectionTitleStyles?: StyleProp; + + /** Decides if selecting with Enter should be disabled */ + turnOffEnterDisabling?: boolean; }; type SelectionListHandle = { diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index 341ea9cddae9..792a1e56ecfc 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -2,7 +2,8 @@ import lodashGet from 'lodash/get'; import React, {useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -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'; @@ -58,20 +59,16 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (policyTag) => policyTag.searchText === selectedTag)[0], 'keyForList'); return ( - { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); + const selectedOptionsNames = (selectedOptions ?? []).map(({name}) => name); + return { text: cleanedName, keyForList: tag.name, searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, + isSelected: selectedOptionsNames.includes(tag.name), }; }); } @@ -1135,7 +1138,7 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt title: '', shouldShow: false, indexOffset, - data: getTagsOptions(selectedTagOptions), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); return tagSections; @@ -1149,7 +1152,7 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt title: '', shouldShow: true, indexOffset, - data: getTagsOptions(searchTags), + data: getTagsOptions(searchTags, selectedOptions), }); return tagSections; @@ -1161,7 +1164,7 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt title: '', shouldShow: false, indexOffset, - data: getTagsOptions(enabledTags), + data: getTagsOptions(enabledTags, selectedOptions), }); return tagSections; @@ -1187,7 +1190,7 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt title: '', shouldShow: true, indexOffset, - data: getTagsOptions(selectedTagOptions), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); indexOffset += selectedOptions.length; @@ -1201,7 +1204,7 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt title: Localize.translateLocal('common.recent'), shouldShow: true, indexOffset, - data: getTagsOptions(cutRecentlyUsedTags), + data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); indexOffset += filteredRecentlyUsedTags.length; @@ -1212,7 +1215,7 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt title: Localize.translateLocal('common.all'), shouldShow: true, indexOffset, - data: getTagsOptions(filteredTags), + data: getTagsOptions(filteredTags, selectedOptions), }); return tagSections; diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index a3d9c2fd99ff..3211efb09a78 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'; @@ -71,6 +72,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, + isSelected: option === fieldValue, })), }); } else { @@ -99,6 +101,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, + isSelected: option === fieldValue, })), }); } @@ -113,6 +116,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, + isSelected: option === fieldValue, })), }); } @@ -130,23 +134,15 @@ 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} /> diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 8c8376468c0f..954c852f365b 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -106,7 +106,7 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) fieldKey={fieldKey} fieldName={Str.UCFirst(reportField.name)} fieldValue={fieldValue} - fieldOptions={reportField.values.filter((value) => !(value in reportField.disabledOptions))} + fieldOptions={reportField.values.filter((_, index) => !reportField.disabledOptions[index])} onSubmit={handleReportFieldChange} /> ); diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 2eb5ecaf373f..9dccf8f911cd 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) { @@ -129,6 +124,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { .map((policy) => ({ text: policy?.name, policyID: policy?.id, + isSelected: policy?.id === activeWorkspaceID, brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), icons: [ { @@ -142,7 +138,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { keyForList: policy?.id, isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), })); - }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline]); + }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline, activeWorkspaceID]); const filteredAndSortedUserWorkspaces = useMemo( () => @@ -237,59 +233,31 @@ 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 + shouldShowTooltips 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 ?? undefined} + turnOffEnterDisabling /> ) : ( )} ), - [ - inputCallbackRef, - setSearchTerm, - searchTerm, - selectPolicy, - selectedOption, - styles, - theme.textSupporting, - translate, - usersWorkspaces.length, - usersWorkspacesSectionData, - activeWorkspaceID, - theme.icon, - headerMessage, - ], + [setSearchTerm, searchTerm, selectPolicy, styles, theme.textSupporting, translate, usersWorkspaces.length, usersWorkspacesSectionData, activeWorkspaceID, theme.icon, headerMessage], ); - 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; } @@ -124,18 +125,18 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} /> - From 966d18ef418fc801be9c4a04d73e7c1e934c6345 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 26 Mar 2024 12:37:12 +0100 Subject: [PATCH 043/127] Fix lint --- src/pages/WorkspaceSwitcherPage.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 9dccf8f911cd..115e1ee25960 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -255,7 +255,20 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { )} ), - [setSearchTerm, searchTerm, selectPolicy, styles, theme.textSupporting, translate, usersWorkspaces.length, usersWorkspacesSectionData, activeWorkspaceID, theme.icon, headerMessage], + [ + inputCallbackRef, + setSearchTerm, + searchTerm, + selectPolicy, + styles, + theme.textSupporting, + translate, + usersWorkspaces.length, + usersWorkspacesSectionData, + activeWorkspaceID, + theme.icon, + headerMessage, + ], ); return ( From 2bf777e96f3e5910e69d974fa8a352c49f947633 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 26 Mar 2024 15:52:52 +0300 Subject: [PATCH 044/127] fix auto focus for natives --- src/libs/ReportActionComposeFocusManager.ts | 6 +++--- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 64e4530e0fe5..7e387d807229 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -3,7 +3,7 @@ import type {TextInput} from 'react-native'; import ROUTES from '@src/ROUTES'; import Navigation from './Navigation/Navigation'; -type FocusCallback = () => void; +type FocusCallback = (shouldFocusForNative?: boolean) => void; const composerRef = React.createRef(); const editComposerRef = React.createRef(); @@ -29,7 +29,7 @@ function onComposerFocus(callback: FocusCallback | null, isMainComposer = false) /** * Request focus on the ReportActionComposer */ -function focus() { +function focus(shouldFocusForNative?: boolean) { /** Do not trigger the refocusing when the active route is not the report route, */ if (!Navigation.isActiveRoute(ROUTES.REPORT_WITH_ID.getRoute(Navigation.getTopmostReportId() ?? ''))) { return; @@ -40,7 +40,7 @@ function focus() { return; } - mainComposerFocusCallback(); + mainComposerFocusCallback(shouldFocusForNative); return; } diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index a270905256d6..d640b3cc6131 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -201,7 +201,7 @@ const ContextMenuActions: ContextMenuAction[] = [ if (closePopover) { hideContextMenu(false, () => { InteractionManager.runAfterInteractions(() => { - ReportActionComposeFocusManager.focus(); + ReportActionComposeFocusManager.focus(true); }); Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); }); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 2f9daa117273..d334d46eaa24 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -602,8 +602,8 @@ function ComposerWithSuggestions( const setUpComposeFocusManager = useCallback(() => { // This callback is used in the contextMenuActions to manage giving focus back to the compose input. - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!willBlurTextInputOnTapOutside || !isFocused) { + ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNative = false) => { + if ((!willBlurTextInputOnTapOutside && !shouldFocusForNative) || !isFocused) { return; } From 5849045587450d2615dffd335922604c48f8a69d Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 26 Mar 2024 21:28:32 +0100 Subject: [PATCH 045/127] Fix failing tests --- tests/unit/OptionsListUtilsTest.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 7244b7830a29..499daf61d2ea 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1120,6 +1120,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, { text: 'HR', @@ -1127,6 +1128,7 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, + isSelected: false, }, { text: 'Medical', @@ -1134,6 +1136,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, + isSelected: false, }, ], }, @@ -1150,6 +1153,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, ], }, @@ -1220,6 +1224,7 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, + isSelected: true, }, ], }, @@ -1234,6 +1239,7 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, + isSelected: false, }, ], }, @@ -1249,6 +1255,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, { text: 'Benefits', @@ -1256,6 +1263,7 @@ describe('OptionsListUtils', () => { searchText: 'Benefits', tooltipText: 'Benefits', isDisabled: false, + isSelected: false, }, { text: 'Cleaning', @@ -1263,6 +1271,7 @@ describe('OptionsListUtils', () => { searchText: 'Cleaning', tooltipText: 'Cleaning', isDisabled: false, + isSelected: false, }, { text: 'Food', @@ -1270,6 +1279,7 @@ describe('OptionsListUtils', () => { searchText: 'Food', tooltipText: 'Food', isDisabled: false, + isSelected: false, }, { text: 'HR', @@ -1277,6 +1287,7 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, + isSelected: false, }, { text: 'Software', @@ -1284,6 +1295,7 @@ describe('OptionsListUtils', () => { searchText: 'Software', tooltipText: 'Software', isDisabled: false, + isSelected: false, }, { text: 'Taxes', @@ -1291,6 +1303,7 @@ describe('OptionsListUtils', () => { searchText: 'Taxes', tooltipText: 'Taxes', isDisabled: false, + isSelected: false, }, ], }, @@ -1307,6 +1320,7 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, + isSelected: false, }, { text: 'Cleaning', @@ -1314,6 +1328,7 @@ describe('OptionsListUtils', () => { searchText: 'Cleaning', tooltipText: 'Cleaning', isDisabled: false, + isSelected: false, }, ], }, From 7950537845073ec3d324ee5d9d475742be67ebd7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 27 Mar 2024 01:28:35 +0500 Subject: [PATCH 046/127] 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 047/127] 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 908363bc083d7b815e587dc4ac24b817a1f64b51 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 27 Mar 2024 08:35:12 +0100 Subject: [PATCH 048/127] CR fixes --- src/components/SelectionList/BaseListItem.tsx | 12 ++---------- src/libs/OptionsListUtils.ts | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 95234dec7dde..1b0f69786b74 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -130,19 +130,11 @@ function BaseListItem({ )} - {!item.isSelected && item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( + {!item.isSelected && item.brickRoadIndicator && [CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR].includes(item.brickRoadIndicator) && ( - - )} - {!item.isSelected && item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO && ( - - )} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 88aa846fbbac..f4caaa9b977d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1101,7 +1101,7 @@ function getTagsOptions(tags: Category[], selectedOptions?: Category[]): Option[ return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); - const selectedOptionsNames = (selectedOptions ?? []).map(({name}) => name); + const selectedOptionsNames = selectedOptions?.map(({name}) => name); return { text: cleanedName, @@ -1109,7 +1109,7 @@ function getTagsOptions(tags: Category[], selectedOptions?: Category[]): Option[ searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, - isSelected: selectedOptionsNames.includes(tag.name), + isSelected: selectedOptionsNames?.includes(tag.name), }; }); } From df13200cdb7d0c0db4a71d0976c5476a74d832c9 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 27 Mar 2024 09:11:17 +0100 Subject: [PATCH 049/127] Merge fix --- 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 098ce0e9355b..c35623e1c63e 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -226,7 +226,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; From 59dff36b466ff6a9f268c2313a9cac71c2db122c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 27 Mar 2024 10:58:31 +0100 Subject: [PATCH 050/127] move isOptionsDataReady outside of useMemo --- .../settings/AboutPage/ShareLogList/BaseShareLogList.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 3fdf503023c4..866f4adaab88 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -24,10 +24,9 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) const {translate} = useLocalize(); const personalDetails = usePersonalDetails(); const betas = useBetas(); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); const searchOptions = useMemo(() => { - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - if (!isOptionsDataReady) { return { recentReports: [], @@ -54,7 +53,7 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) userToInvite: localUserToInvite, headerMessage: header, }; - }, [betas, personalDetails, reports, debouncedSearchValue]); + }, [isOptionsDataReady, reports, personalDetails, debouncedSearchValue, betas]); const sections = useMemo(() => { const sectionsList = []; From 8c0297b00c1c60d790f8b45af19bac31a5d4cc4a Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 27 Mar 2024 12:04:28 +0100 Subject: [PATCH 051/127] Fix isSelected state in EditReportFieldDropdownPage --- src/pages/EditReportFieldDropdownPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index d13233aeab80..b82589c03bf9 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -43,6 +43,7 @@ type ReportFieldDropdownData = { keyForList: string; searchText: string; tooltipText: string; + isSelected?: boolean; }; type ReportFieldDropdownSectionItem = { @@ -72,7 +73,6 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, - isSelected: option === fieldValue, })), }); } else { @@ -86,6 +86,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: selectedValue, searchText: selectedValue, tooltipText: selectedValue, + isSelected: true, }, ], }); @@ -101,7 +102,6 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, - isSelected: option === fieldValue, })), }); } @@ -116,7 +116,6 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, - isSelected: option === fieldValue, })), }); } From 82445012d0fb8f0a695b13df50409e5361dbdb2f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 28 Mar 2024 18:41:14 +0000 Subject: [PATCH 052/127] Reapply "[No QA] Migrate 'OptionsListUtilsTest.js', 'DateUtilsTest.js', 'SidebarLinks.perf-test.js', 'markdown.js' and 'ReportUtilsTest.js' to Typescript" This reverts commit 39aaa567d352e8a43538886ea614c745d1b8c03a. --- src/libs/DateUtils.ts | 2 +- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 6 +- tests/e2e/compare/output/console.ts | 4 +- .../output/{markdown.js => markdown.ts} | 65 +-- ...erf-test.js => SidebarLinks.perf-test.tsx} | 39 +- .../{DateUtilsTest.js => DateUtilsTest.ts} | 57 +-- ...stUtilsTest.js => OptionsListUtilsTest.ts} | 389 ++++++++++++------ ...{ReportUtilsTest.js => ReportUtilsTest.ts} | 202 +++++---- 9 files changed, 479 insertions(+), 287 deletions(-) rename tests/e2e/compare/output/{markdown.js => markdown.ts} (57%) rename tests/perf-test/{SidebarLinks.perf-test.js => SidebarLinks.perf-test.tsx} (79%) rename tests/unit/{DateUtilsTest.js => DateUtilsTest.ts} (85%) rename tests/unit/{OptionsListUtilsTest.js => OptionsListUtilsTest.ts} (86%) rename tests/unit/{ReportUtilsTest.js => ReportUtilsTest.ts} (84%) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 4d4f8d425681..44c7682b47f2 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -267,7 +267,7 @@ function formatToLongDateWithWeekday(datetime: string | Date): string { * @returns Sunday */ function formatToDayOfWeek(datetime: Date): string { - return format(new Date(datetime), CONST.DATE.WEEKDAY_TIME_FORMAT); + return format(datetime, CONST.DATE.WEEKDAY_TIME_FORMAT); } /** diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index cf988eb8aef5..ab057a4c10ec 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2095,4 +2095,4 @@ export { getTaxRatesSection, }; -export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, CategoryTreeSection, GetOptions, PayeePersonalDetails, Category, Tag}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ea6f08a3a2b1..a76967d00d21 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -28,6 +28,7 @@ import type { ReportMetadata, Session, Task, + TaxRate, Transaction, TransactionViolation, UserWallet, @@ -405,6 +406,9 @@ type OptionData = { isDisabled?: boolean | null; name?: string | null; isSelfDM?: boolean | null; + reportID?: string; + enabled?: boolean; + data?: Partial; } & Report; type OnyxDataTaskAssigneeChat = { @@ -959,7 +963,7 @@ function filterReportsByPolicyIDAndMemberAccountIDs(reports: Report[], policyMem /** * Given an array of reports, return them sorted by the last read timestamp. */ -function sortReportsByLastRead(reports: Report[], reportMetadata: OnyxCollection): Array> { +function sortReportsByLastRead(reports: Array>, reportMetadata: OnyxCollection): Array> { return reports .filter((report) => !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime)) .sort((a, b) => { diff --git a/tests/e2e/compare/output/console.ts b/tests/e2e/compare/output/console.ts index de8e5d913893..3da0100b603f 100644 --- a/tests/e2e/compare/output/console.ts +++ b/tests/e2e/compare/output/console.ts @@ -13,6 +13,8 @@ type Entry = { type Data = { significance: Entry[]; meaningless: Entry[]; + errors: string[]; + warnings: string[]; }; const printRegularLine = (entry: Entry) => { @@ -36,4 +38,4 @@ export default (data: Data) => { console.debug(''); }; -export type {Entry}; +export type {Data, Entry}; diff --git a/tests/e2e/compare/output/markdown.js b/tests/e2e/compare/output/markdown.ts similarity index 57% rename from tests/e2e/compare/output/markdown.js rename to tests/e2e/compare/output/markdown.ts index 119830a5bb2c..34bc3251c422 100644 --- a/tests/e2e/compare/output/markdown.js +++ b/tests/e2e/compare/output/markdown.ts @@ -1,80 +1,85 @@ // From: https://raw.githubusercontent.com/callstack/reassure/main/packages/reassure-compare/src/output/markdown.ts import fs from 'node:fs/promises'; import path from 'path'; -import _ from 'underscore'; +import type {Stats} from 'tests/e2e/measure/math'; import * as Logger from '../../utils/logger'; +import type {Data, Entry} from './console'; import * as format from './format'; import markdownTable from './markdownTable'; const tableHeader = ['Name', 'Duration']; -const collapsibleSection = (title, content) => `
\n${title}\n\n${content}\n
\n\n`; +const collapsibleSection = (title: string, content: string) => `
\n${title}\n\n${content}\n
\n\n`; -const buildDurationDetails = (title, entry) => { +const buildDurationDetails = (title: string, entry: Stats) => { const relativeStdev = entry.stdev / entry.mean; - return _.filter( - [ - `**${title}**`, - `Mean: ${format.formatDuration(entry.mean)}`, - `Stdev: ${format.formatDuration(entry.stdev)} (${format.formatPercent(relativeStdev)})`, - entry.entries ? `Runs: ${entry.entries.join(' ')}` : '', - ], - Boolean, - ).join('
'); + return [ + `**${title}**`, + `Mean: ${format.formatDuration(entry.mean)}`, + `Stdev: ${format.formatDuration(entry.stdev)} (${format.formatPercent(relativeStdev)})`, + entry.entries ? `Runs: ${entry.entries.join(' ')}` : '', + ] + .filter(Boolean) + .join('
'); }; -const buildDurationDetailsEntry = (entry) => - _.filter(['baseline' in entry ? buildDurationDetails('Baseline', entry.baseline) : '', 'current' in entry ? buildDurationDetails('Current', entry.current) : ''], Boolean).join( - '

', - ); +const buildDurationDetailsEntry = (entry: Entry) => + ['baseline' in entry ? buildDurationDetails('Baseline', entry.baseline) : '', 'current' in entry ? buildDurationDetails('Current', entry.current) : ''] + .filter(Boolean) + .join('

'); + +const formatEntryDuration = (entry: Entry): string => { + let formattedDuration = ''; -const formatEntryDuration = (entry) => { if ('baseline' in entry && 'current' in entry) { - return format.formatDurationDiffChange(entry); + formattedDuration = format.formatDurationDiffChange(entry); } + if ('baseline' in entry) { - return format.formatDuration(entry.baseline.mean); + formattedDuration = format.formatDuration(entry.baseline.mean); } + if ('current' in entry) { - return format.formatDuration(entry.current.mean); + formattedDuration = format.formatDuration(entry.current.mean); } - return ''; + + return formattedDuration; }; -const buildDetailsTable = (entries) => { +const buildDetailsTable = (entries: Entry[]) => { if (!entries.length) { return ''; } - const rows = _.map(entries, (entry) => [entry.name, buildDurationDetailsEntry(entry)]); + const rows = entries.map((entry) => [entry.name, buildDurationDetailsEntry(entry)]); const content = markdownTable([tableHeader, ...rows]); return collapsibleSection('Show details', content); }; -const buildSummaryTable = (entries, collapse = false) => { +const buildSummaryTable = (entries: Entry[], collapse = false) => { if (!entries.length) { return '_There are no entries_'; } - const rows = _.map(entries, (entry) => [entry.name, formatEntryDuration(entry)]); + const rows = entries.map((entry) => [entry.name, formatEntryDuration(entry)]); const content = markdownTable([tableHeader, ...rows]); return collapse ? collapsibleSection('Show entries', content) : content; }; -const buildMarkdown = (data) => { +const buildMarkdown = (data: Data) => { let result = '## Performance Comparison Report 📊'; - if (data.errors && data.errors.length) { + if (data.errors?.length) { result += '\n\n### Errors\n'; data.errors.forEach((message) => { result += ` 1. 🛑 ${message}\n`; }); } - if (data.warnings && data.warnings.length) { + if (data.warnings?.length) { result += '\n\n### Warnings\n'; data.warnings.forEach((message) => { result += ` 1. 🟡 ${message}\n`; @@ -92,7 +97,7 @@ const buildMarkdown = (data) => { return result; }; -const writeToFile = (filePath, content) => +const writeToFile = (filePath: string, content: string) => fs .writeFile(filePath, content) .then(() => { @@ -106,7 +111,7 @@ const writeToFile = (filePath, content) => throw error; }); -const writeToMarkdown = (filePath, data) => { +const writeToMarkdown = (filePath: string, data: Data) => { const markdown = buildMarkdown(data); return writeToFile(filePath, markdown).catch((error) => { console.error(error); diff --git a/tests/perf-test/SidebarLinks.perf-test.js b/tests/perf-test/SidebarLinks.perf-test.tsx similarity index 79% rename from tests/perf-test/SidebarLinks.perf-test.js rename to tests/perf-test/SidebarLinks.perf-test.tsx index 0b10718fd0c4..2848015d5c63 100644 --- a/tests/perf-test/SidebarLinks.perf-test.js +++ b/tests/perf-test/SidebarLinks.perf-test.tsx @@ -1,32 +1,33 @@ import {fireEvent, screen} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import {measurePerformance} from 'reassure'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import ONYXKEYS from '../../src/ONYXKEYS'; -import variables from '../../src/styles/variables'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -jest.mock('../../src/libs/Permissions'); -jest.mock('../../src/hooks/usePermissions.ts'); -jest.mock('../../src/libs/Navigation/Navigation'); -jest.mock('../../src/components/Icon/Expensicons'); +jest.mock('@libs/Permissions'); +jest.mock('@hooks/usePermissions.ts'); +jest.mock('@libs/Navigation/Navigation'); +jest.mock('@components/Icon/Expensicons'); jest.mock('@react-navigation/native'); const getMockedReportsMap = (length = 100) => { - const mockReports = Array.from({length}, (__, i) => { - const reportID = i + 1; - const participants = [1, 2]; - const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - const report = LHNTestUtils.getFakeReport(participants, 1, true); - - return {[reportKey]: report}; - }); - - return _.assign({}, ...mockReports); + const mockReports = Object.fromEntries( + Array.from({length}, (value, index) => { + const reportID = index + 1; + const participants = [1, 2]; + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + const report = LHNTestUtils.getFakeReport(participants, 1, true); + + return [reportKey, report]; + }), + ); + + return mockReports; }; const mockedResponseMap = getMockedReportsMap(500); @@ -36,11 +37,9 @@ describe('SidebarLinks', () => { Onyx.init({ keys: ONYXKEYS, safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], - registerStorageEventListener: () => {}, }); Onyx.multiSet({ - [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.ts similarity index 85% rename from tests/unit/DateUtilsTest.js rename to tests/unit/DateUtilsTest.ts index a752eea1a990..a7f43ea84045 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import {addDays, addMinutes, format, setHours, setMinutes, subDays, subHours, subMinutes, subSeconds} from 'date-fns'; import {format as tzFormat, utcToZonedTime} from 'date-fns-tz'; import Onyx from 'react-native-onyx'; -import CONST from '../../src/CONST'; -import DateUtils from '../../src/libs/DateUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const LOCALE = CONST.LOCALES.EN; @@ -14,13 +16,14 @@ describe('DateUtils', () => { keys: ONYXKEYS, initialKeyStates: { [ONYXKEYS.SESSION]: {accountID: 999}, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: {999: {timezone: {selected: UTC}}}, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {'999': {accountID: 999, timezone: {selected: 'Europe/London'}}}, }, }); return waitForBatchedUpdates(); }); afterEach(() => { + jest.restoreAllMocks(); jest.useRealTimers(); Onyx.clear(); }); @@ -39,7 +42,7 @@ describe('DateUtils', () => { }); it('formatToDayOfWeek should return a weekday', () => { - const weekDay = DateUtils.formatToDayOfWeek(datetime); + const weekDay = DateUtils.formatToDayOfWeek(new Date(datetime)); expect(weekDay).toBe('Monday'); }); it('formatToLocalTime should return a date in a local format', () => { @@ -53,32 +56,35 @@ describe('DateUtils', () => { }); it('should fallback to current date when getLocalDateFromDatetime is failing', () => { - const localDate = DateUtils.getLocalDateFromDatetime(LOCALE, undefined, 'InvalidTimezone'); + const localDate = DateUtils.getLocalDateFromDatetime(LOCALE, undefined, 'InvalidTimezone' as SelectedTimezone); expect(localDate.getTime()).not.toBeNaN(); }); it('should return the date in calendar time when calling datetimeToCalendarTime', () => { - const today = setMinutes(setHours(new Date(), 14), 32); + const today = setMinutes(setHours(new Date(), 14), 32).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, today)).toBe('Today at 2:32 PM'); - const tomorrow = addDays(setMinutes(setHours(new Date(), 14), 32), 1); + const tomorrow = addDays(setMinutes(setHours(new Date(), 14), 32), 1).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, tomorrow)).toBe('Tomorrow at 2:32 PM'); - const yesterday = setMinutes(setHours(subDays(new Date(), 1), 7), 43); + const yesterday = setMinutes(setHours(subDays(new Date(), 1), 7), 43).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, yesterday)).toBe('Yesterday at 7:43 AM'); - const date = setMinutes(setHours(new Date('2022-11-05'), 10), 17); + const date = setMinutes(setHours(new Date('2022-11-05'), 10), 17).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, date)).toBe('Nov 5, 2022 at 10:17 AM'); - const todayLowercaseDate = setMinutes(setHours(new Date(), 14), 32); + const todayLowercaseDate = setMinutes(setHours(new Date(), 14), 32).toString(); expect(DateUtils.datetimeToCalendarTime(LOCALE, todayLowercaseDate, false, undefined, true)).toBe('today at 2:32 PM'); }); it('should update timezone if automatic and selected timezone do not match', () => { - Intl.DateTimeFormat = jest.fn(() => ({ - resolvedOptions: () => ({timeZone: 'America/Chicago'}), - })); - Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {999: {timezone: {selected: UTC, automatic: true}}}).then(() => { + jest.spyOn(Intl, 'DateTimeFormat').mockImplementation( + () => + ({ + resolvedOptions: () => ({timeZone: 'America/Chicago'}), + } as Intl.DateTimeFormat), + ); + Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {'999': {accountID: 999, timezone: {selected: 'Europe/London', automatic: true}}}).then(() => { const result = DateUtils.getCurrentTimezone(); expect(result).toEqual({ selected: 'America/Chicago', @@ -88,10 +94,13 @@ describe('DateUtils', () => { }); it('should not update timezone if automatic and selected timezone match', () => { - Intl.DateTimeFormat = jest.fn(() => ({ - resolvedOptions: () => ({timeZone: UTC}), - })); - Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {999: {timezone: {selected: UTC, automatic: true}}}).then(() => { + jest.spyOn(Intl, 'DateTimeFormat').mockImplementation( + () => + ({ + resolvedOptions: () => ({timeZone: UTC}), + } as Intl.DateTimeFormat), + ); + Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {'999': {accountID: 999, timezone: {selected: 'Europe/London', automatic: true}}}).then(() => { const result = DateUtils.getCurrentTimezone(); expect(result).toEqual({ selected: UTC, @@ -102,7 +111,7 @@ describe('DateUtils', () => { it('canUpdateTimezone should return true when lastUpdatedTimezoneTime is more than 5 minutes ago', () => { // Use fake timers to control the current time - jest.useFakeTimers('modern'); + jest.useFakeTimers(); jest.setSystemTime(addMinutes(new Date(), 6)); const isUpdateTimezoneAllowed = DateUtils.canUpdateTimezone(); expect(isUpdateTimezoneAllowed).toBe(true); @@ -110,20 +119,20 @@ describe('DateUtils', () => { it('canUpdateTimezone should return false when lastUpdatedTimezoneTime is less than 5 minutes ago', () => { // Use fake timers to control the current time - jest.useFakeTimers('modern'); + jest.useFakeTimers(); jest.setSystemTime(addMinutes(new Date(), 4)); const isUpdateTimezoneAllowed = DateUtils.canUpdateTimezone(); expect(isUpdateTimezoneAllowed).toBe(false); }); it('should return the date in calendar time when calling datetimeToRelative', () => { - const aFewSecondsAgo = subSeconds(new Date(), 10); + const aFewSecondsAgo = subSeconds(new Date(), 10).toString(); expect(DateUtils.datetimeToRelative(LOCALE, aFewSecondsAgo)).toBe('less than a minute ago'); - const aMinuteAgo = subMinutes(new Date(), 1); + const aMinuteAgo = subMinutes(new Date(), 1).toString(); expect(DateUtils.datetimeToRelative(LOCALE, aMinuteAgo)).toBe('1 minute ago'); - const anHourAgo = subHours(new Date(), 1); + const anHourAgo = subHours(new Date(), 1).toString(); expect(DateUtils.datetimeToRelative(LOCALE, anHourAgo)).toBe('about 1 hour ago'); }); diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.ts similarity index 86% rename from tests/unit/OptionsListUtilsTest.js rename to tests/unit/OptionsListUtilsTest.ts index 2f3e65c0c384..afa9b00ebc6a 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1,30 +1,35 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; -import * as OptionsListUtils from '../../src/libs/OptionsListUtils'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import type {Tag} from '@src/libs/OptionsListUtils'; +import * as OptionsListUtils from '@src/libs/OptionsListUtils'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetails, Policy, PolicyCategories, Report, TaxRatesWithDefault} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +type PersonalDetailsList = Record; + describe('OptionsListUtils', () => { // Given a set of reports with both single participants and multiple participants some pinned and some not - const REPORTS = { - 1: { + const REPORTS: OnyxCollection = { + '1': { lastReadTime: '2021-01-14 11:25:39.295', lastVisibleActionCreated: '2022-11-22 03:26:02.015', isPinned: false, - reportID: 1, + reportID: '1', participantAccountIDs: [2, 1], visibleChatMemberAccountIDs: [2, 1], reportName: 'Iron Man, Mister Fantastic', hasDraft: true, type: CONST.REPORT.TYPE.CHAT, }, - 2: { + '2': { lastReadTime: '2021-01-14 11:25:39.296', lastVisibleActionCreated: '2022-11-22 03:26:02.016', isPinned: false, - reportID: 2, + reportID: '2', participantAccountIDs: [3], visibleChatMemberAccountIDs: [3], reportName: 'Spider-Man', @@ -32,41 +37,41 @@ describe('OptionsListUtils', () => { }, // This is the only report we are pinning in this test - 3: { + '3': { lastReadTime: '2021-01-14 11:25:39.297', lastVisibleActionCreated: '2022-11-22 03:26:02.170', isPinned: true, - reportID: 3, + reportID: '3', participantAccountIDs: [1], visibleChatMemberAccountIDs: [1], reportName: 'Mister Fantastic', type: CONST.REPORT.TYPE.CHAT, }, - 4: { + '4': { lastReadTime: '2021-01-14 11:25:39.298', lastVisibleActionCreated: '2022-11-22 03:26:02.180', isPinned: false, - reportID: 4, + reportID: '4', participantAccountIDs: [4], visibleChatMemberAccountIDs: [4], reportName: 'Black Panther', type: CONST.REPORT.TYPE.CHAT, }, - 5: { + '5': { lastReadTime: '2021-01-14 11:25:39.299', lastVisibleActionCreated: '2022-11-22 03:26:02.019', isPinned: false, - reportID: 5, + reportID: '5', participantAccountIDs: [5], visibleChatMemberAccountIDs: [5], reportName: 'Invisible Woman', type: CONST.REPORT.TYPE.CHAT, }, - 6: { + '6': { lastReadTime: '2021-01-14 11:25:39.300', lastVisibleActionCreated: '2022-11-22 03:26:02.020', isPinned: false, - reportID: 6, + reportID: '6', participantAccountIDs: [6], visibleChatMemberAccountIDs: [6], reportName: 'Thor', @@ -74,11 +79,11 @@ describe('OptionsListUtils', () => { }, // Note: This report has the largest lastVisibleActionCreated - 7: { + '7': { lastReadTime: '2021-01-14 11:25:39.301', lastVisibleActionCreated: '2022-11-22 03:26:03.999', isPinned: false, - reportID: 7, + reportID: '7', participantAccountIDs: [7], visibleChatMemberAccountIDs: [7], reportName: 'Captain America', @@ -86,11 +91,11 @@ describe('OptionsListUtils', () => { }, // Note: This report has no lastVisibleActionCreated - 8: { + '8': { lastReadTime: '2021-01-14 11:25:39.301', lastVisibleActionCreated: '2022-11-22 03:26:02.000', isPinned: false, - reportID: 8, + reportID: '8', participantAccountIDs: [12], visibleChatMemberAccountIDs: [12], reportName: 'Silver Surfer', @@ -98,23 +103,23 @@ describe('OptionsListUtils', () => { }, // Note: This report has an IOU - 9: { + '9': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.998', isPinned: false, - reportID: 9, + reportID: '9', participantAccountIDs: [8], visibleChatMemberAccountIDs: [8], reportName: 'Mister Sinister', - iouReportID: 100, + iouReportID: '100', type: CONST.REPORT.TYPE.CHAT, }, // This report is an archived room – it does not have a name and instead falls back on oldPolicyName - 10: { + '10': { lastReadTime: '2021-01-14 11:25:39.200', lastVisibleActionCreated: '2022-11-22 03:26:02.001', - reportID: 10, + reportID: '10', isPinned: false, participantAccountIDs: [2, 7], visibleChatMemberAccountIDs: [2, 7], @@ -131,71 +136,81 @@ describe('OptionsListUtils', () => { }; // And a set of personalDetails some with existing reports and some without - const PERSONAL_DETAILS = { + const PERSONAL_DETAILS: PersonalDetailsList = { // These exist in our reports - 1: { + '1': { accountID: 1, displayName: 'Mister Fantastic', login: 'reedrichards@expensify.com', isSelected: true, + reportID: '1', }, - 2: { + '2': { accountID: 2, displayName: 'Iron Man', login: 'tonystark@expensify.com', + reportID: '1', }, - 3: { + '3': { accountID: 3, displayName: 'Spider-Man', login: 'peterparker@expensify.com', + reportID: '1', }, - 4: { + '4': { accountID: 4, displayName: 'Black Panther', login: 'tchalla@expensify.com', + reportID: '1', }, - 5: { + '5': { accountID: 5, displayName: 'Invisible Woman', login: 'suestorm@expensify.com', + reportID: '1', }, - 6: { + '6': { accountID: 6, displayName: 'Thor', login: 'thor@expensify.com', + reportID: '1', }, - 7: { + '7': { accountID: 7, displayName: 'Captain America', login: 'steverogers@expensify.com', + reportID: '1', }, - 8: { + '8': { accountID: 8, displayName: 'Mr Sinister', login: 'mistersinister@marauders.com', + reportID: '1', }, // These do not exist in reports at all - 9: { + '9': { accountID: 9, displayName: 'Black Widow', login: 'natasharomanoff@expensify.com', + reportID: '', }, - 10: { + '10': { accountID: 10, displayName: 'The Incredible Hulk', login: 'brucebanner@expensify.com', + reportID: '', }, }; - const REPORTS_WITH_CONCIERGE = { + const REPORTS_WITH_CONCIERGE: OnyxCollection = { ...REPORTS, - 11: { + '11': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 11, + reportID: '11', participantAccountIDs: [999], visibleChatMemberAccountIDs: [999], reportName: 'Concierge', @@ -203,13 +218,13 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_CHRONOS = { + const REPORTS_WITH_CHRONOS: OnyxCollection = { ...REPORTS, - 12: { + '12': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 12, + reportID: '12', participantAccountIDs: [1000], visibleChatMemberAccountIDs: [1000], reportName: 'Chronos', @@ -217,13 +232,13 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_RECEIPTS = { + const REPORTS_WITH_RECEIPTS: OnyxCollection = { ...REPORTS, - 13: { + '13': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 13, + reportID: '13', participantAccountIDs: [1001], visibleChatMemberAccountIDs: [1001], reportName: 'Receipts', @@ -231,67 +246,77 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_WORKSPACE_ROOMS = { + const REPORTS_WITH_WORKSPACE_ROOMS: OnyxCollection = { ...REPORTS, - 14: { + '14': { lastReadTime: '2021-01-14 11:25:39.302', lastVisibleActionCreated: '2022-11-22 03:26:02.022', isPinned: false, - reportID: 14, + reportID: '14', participantAccountIDs: [1, 10, 3], visibleChatMemberAccountIDs: [1, 10, 3], reportName: '', oldPolicyName: 'Avengers Room', - isArchivedRoom: false, chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, isOwnPolicyExpenseChat: true, type: CONST.REPORT.TYPE.CHAT, }, }; - const PERSONAL_DETAILS_WITH_CONCIERGE = { + const PERSONAL_DETAILS_WITH_CONCIERGE: PersonalDetailsList = { ...PERSONAL_DETAILS, - 999: { + '999': { accountID: 999, displayName: 'Concierge', login: 'concierge@expensify.com', + reportID: '', }, }; - const PERSONAL_DETAILS_WITH_CHRONOS = { + const PERSONAL_DETAILS_WITH_CHRONOS: PersonalDetailsList = { ...PERSONAL_DETAILS, - 1000: { + '1000': { accountID: 1000, displayName: 'Chronos', login: 'chronos@expensify.com', + reportID: '', }, }; - const PERSONAL_DETAILS_WITH_RECEIPTS = { + const PERSONAL_DETAILS_WITH_RECEIPTS: PersonalDetailsList = { ...PERSONAL_DETAILS, - 1001: { + '1001': { accountID: 1001, displayName: 'Receipts', login: 'receipts@expensify.com', + reportID: '', }, }; - const PERSONAL_DETAILS_WITH_PERIODS = { + const PERSONAL_DETAILS_WITH_PERIODS: PersonalDetailsList = { ...PERSONAL_DETAILS, - 1002: { + '1002': { accountID: 1002, displayName: 'The Flash', login: 'barry.allen@expensify.com', + reportID: '', }, }; - const POLICY = { - policyID: 'ABC123', + const policyID = 'ABC123'; + + const POLICY: Policy = { + id: policyID, name: 'Hero Policy', + role: 'user', + type: 'free', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, }; // Set the currently logged in user, report data, and personal details @@ -300,11 +325,12 @@ describe('OptionsListUtils', () => { keys: ONYXKEYS, initialKeyStates: { [ONYXKEYS.SESSION]: {accountID: 2, email: 'tonystark@expensify.com'}, - [`${ONYXKEYS.COLLECTION.REPORT}100`]: { + [`${ONYXKEYS.COLLECTION.REPORT}100` as const]: { + reportID: '', ownerAccountID: 8, - total: '1000', + total: 1000, }, - [`${ONYXKEYS.COLLECTION.POLICY}${POLICY.policyID}`]: POLICY, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: POLICY, }, }); Onyx.registerLogger(() => {}); @@ -319,7 +345,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(2); // Then all of the reports should be shown including the archived rooms. - expect(results.recentReports.length).toBe(_.size(REPORTS)); + expect(results.recentReports.length).toBe(Object.values(REPORTS).length); // When we filter again but provide a searchValue results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS, 'spider'); @@ -360,7 +386,7 @@ describe('OptionsListUtils', () => { // We should expect all personalDetails to be returned, // minus the currently logged in user and recent reports count - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS).length - 1 - MAX_RECENT_REPORTS); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -369,11 +395,11 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('The Incredible Hulk'); // Then the result which has an existing report should also have the reportID attached - const personalDetailWithExistingReport = _.find(results.personalDetails, (personalDetail) => personalDetail.login === 'peterparker@expensify.com'); - expect(personalDetailWithExistingReport.reportID).toBe(2); + const personalDetailWithExistingReport = results.personalDetails.find((personalDetail) => personalDetail.login === 'peterparker@expensify.com'); + expect(personalDetailWithExistingReport?.reportID).toBe('2'); // When we only pass personal details - results = OptionsListUtils.getFilteredOptions([], PERSONAL_DETAILS, [], ''); + results = OptionsListUtils.getFilteredOptions({}, PERSONAL_DETAILS, [], ''); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -414,28 +440,28 @@ describe('OptionsListUtils', () => { // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 1 - MAX_RECENT_REPORTS); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CHRONOS).length - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); // All the personalDetails should be returned minus the currently logged in user and Concierge - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 2 - MAX_RECENT_REPORTS); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_RECEIPTS).length - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); @@ -448,7 +474,7 @@ describe('OptionsListUtils', () => { // And we should expect all the personalDetails to show (minus the 5 that are already // showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 6); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS).length - 6); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Widow'); @@ -457,8 +483,8 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('The Incredible Hulk'); // And none of our personalDetails should include any of the users with recent reports - const reportLogins = _.map(results.recentReports, (reportOption) => reportOption.login); - const personalDetailsOverlapWithReports = _.every(results.personalDetails, (personalDetailOption) => _.contains(reportLogins, personalDetailOption.login)); + const reportLogins = results.recentReports.map((reportOption) => reportOption.login); + const personalDetailsOverlapWithReports = results.personalDetails.every((personalDetailOption) => reportLogins.includes(personalDetailOption.login)); expect(personalDetailsOverlapWithReports).toBe(false); // When we search for an option that is only in a personalDetail with no existing report @@ -487,15 +513,15 @@ describe('OptionsListUtils', () => { // Then one of our older report options (not in our five most recent) should appear in the personalDetails // but not in recentReports - expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); - expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(false); + expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); + expect(results.personalDetails.every((option) => option.login !== 'peterparker@expensify.com')).toBe(false); // When we provide a "selected" option to getFilteredOptions() results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]); // Then the option should not appear anywhere in either list - expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); - expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); + expect(results.recentReports.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); + expect(results.personalDetails.every((option) => option.login !== 'peterparker@expensify.com')).toBe(true); // When we add a search term for which no options exist and the searchValue itself // is not a potential email or phone @@ -531,7 +557,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(0); expect(results.personalDetails.length).toBe(0); expect(results.userToInvite).not.toBe(null); - expect(results.userToInvite.login).toBe('+15005550006'); + expect(results.userToInvite?.login).toBe('+15005550006'); // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with country code added @@ -542,7 +568,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(0); expect(results.personalDetails.length).toBe(0); expect(results.userToInvite).not.toBe(null); - expect(results.userToInvite.login).toBe('+15005550006'); + expect(results.userToInvite?.login).toBe('+15005550006'); // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with special characters added @@ -553,7 +579,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(0); expect(results.personalDetails.length).toBe(0); expect(results.userToInvite).not.toBe(null); - expect(results.userToInvite.login).toBe('+18003243233'); + expect(results.userToInvite?.login).toBe('+18003243233'); // When we use a search term for contact number that contains alphabet characters results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa'); @@ -568,7 +594,7 @@ describe('OptionsListUtils', () => { // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 6); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 6); expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results @@ -576,7 +602,7 @@ describe('OptionsListUtils', () => { // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 7); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CONCIERGE).length - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); @@ -585,7 +611,7 @@ describe('OptionsListUtils', () => { // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 7); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_CHRONOS).length - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); @@ -594,26 +620,27 @@ describe('OptionsListUtils', () => { // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) - expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 7); + expect(results.personalDetails.length).toBe(Object.values(PERSONAL_DETAILS_WITH_RECEIPTS).length - 7); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); it('getShareDestinationsOptions()', () => { // Filter current REPORTS as we do in the component, before getting share destination options - const filteredReports = {}; - _.keys(REPORTS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS[reportKey])) { - return; + const filteredReports = Object.entries(REPORTS).reduce>>((reports, [reportKey, report]) => { + if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + return reports; } - filteredReports[reportKey] = REPORTS[reportKey]; - }); + // eslint-disable-next-line no-param-reassign + reports[reportKey] = report; + return reports; + }, {}); // When we pass an empty search value let results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], ''); // Then we should expect all the recent reports to show but exclude the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS) - 1); + expect(results.recentReports.length).toBe(Object.values(REPORTS).length - 1); // When we pass a search value that doesn't match the group chat name results = OptionsListUtils.getShareDestinationOptions(filteredReports, PERSONAL_DETAILS, [], 'mutants'); @@ -628,20 +655,19 @@ describe('OptionsListUtils', () => { expect(results.recentReports.length).toBe(1); // Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options - const filteredReportsWithWorkspaceRooms = {}; - _.keys(REPORTS_WITH_WORKSPACE_ROOMS).forEach((reportKey) => { - if (!ReportUtils.canUserPerformWriteAction(REPORTS_WITH_WORKSPACE_ROOMS[reportKey]) || ReportUtils.isExpensifyOnlyParticipantInReport(REPORTS_WITH_WORKSPACE_ROOMS[reportKey])) { - return; + const filteredReportsWithWorkspaceRooms = Object.entries(REPORTS_WITH_WORKSPACE_ROOMS).reduce>>((reports, [reportKey, report]) => { + if (!ReportUtils.canUserPerformWriteAction(report) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + return reports; } - filteredReportsWithWorkspaceRooms[reportKey] = REPORTS_WITH_WORKSPACE_ROOMS[reportKey]; - }); + return {...reports, [reportKey]: report}; + }, {}); // When we also have a policy to return rooms in the results results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], ''); // Then we should expect the DMS, the group chats and the workspace room to show // We should expect all the recent reports to show, excluding the archived rooms - expect(results.recentReports.length).toBe(_.size(REPORTS_WITH_WORKSPACE_ROOMS) - 1); + expect(results.recentReports.length).toBe(Object.values(REPORTS_WITH_WORKSPACE_ROOMS).length - 1); // When we search for a workspace room results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, PERSONAL_DETAILS, [], 'Avengers Room'); @@ -685,31 +711,51 @@ describe('OptionsListUtils', () => { const emptySearch = ''; const wrongSearch = 'bla bla'; const recentlyUsedCategories = ['Taxi', 'Restaurant']; - const selectedOptions = [ + const selectedOptions: Array> = [ { name: 'Medical', enabled: true, }, ]; - const smallCategoriesList = { + const smallCategoriesList: PolicyCategories = { Taxi: { enabled: false, name: 'Taxi', + unencodedName: 'Taxi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Restaurant: { enabled: true, name: 'Restaurant', + unencodedName: 'Restaurant', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Food: { enabled: true, name: 'Food', + unencodedName: 'Food', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Meat': { enabled: true, name: 'Food: Meat', + unencodedName: 'Food: Meat', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, }; - const smallResultList = [ + const smallResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: false, @@ -742,7 +788,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallSearchResultList = [ + const smallSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -767,7 +813,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList = [ + const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -775,65 +821,135 @@ describe('OptionsListUtils', () => { data: [], }, ]; - const largeCategoriesList = { + const largeCategoriesList: PolicyCategories = { Taxi: { enabled: false, name: 'Taxi', + unencodedName: 'Taxi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Restaurant: { enabled: true, name: 'Restaurant', + unencodedName: 'Restaurant', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Food: { enabled: true, name: 'Food', + unencodedName: 'Food', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Meat': { enabled: true, name: 'Food: Meat', + unencodedName: 'Food: Meat', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Milk': { enabled: true, name: 'Food: Milk', + unencodedName: 'Food: Milk', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Food: Vegetables': { enabled: false, name: 'Food: Vegetables', + unencodedName: 'Food: Vegetables', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Cars: Audi': { enabled: true, name: 'Cars: Audi', + unencodedName: 'Cars: Audi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Cars: BMW': { enabled: false, name: 'Cars: BMW', + unencodedName: 'Cars: BMW', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Cars: Mercedes-Benz': { enabled: true, name: 'Cars: Mercedes-Benz', + unencodedName: 'Cars: Mercedes-Benz', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, Medical: { enabled: false, name: 'Medical', + unencodedName: 'Medical', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals': { enabled: true, name: 'Travel: Meals', + unencodedName: 'Travel: Meals', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals: Breakfast': { enabled: true, name: 'Travel: Meals: Breakfast', + unencodedName: 'Travel: Meals: Breakfast', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals: Dinner': { enabled: false, name: 'Travel: Meals: Dinner', + unencodedName: 'Travel: Meals: Dinner', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, 'Travel: Meals: Lunch': { enabled: true, name: 'Travel: Meals: Lunch', + unencodedName: 'Travel: Meals: Lunch', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', }, }; - const largeResultList = [ + const largeResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: false, @@ -960,7 +1076,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeSearchResultList = [ + const largeSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -993,7 +1109,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList = [ + const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -1002,7 +1118,7 @@ describe('OptionsListUtils', () => { }, ]; const emptyCategoriesList = {}; - const emptySelectedResultList = [ + const emptySelectedResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: false, @@ -1089,25 +1205,29 @@ describe('OptionsListUtils', () => { name: 'Medical', }, ]; - const smallTagsList = { + const smallTagsList: Record = { Engineering: { enabled: false, name: 'Engineering', + accountID: null, }, Medical: { enabled: true, name: 'Medical', + accountID: null, }, Accounting: { enabled: true, name: 'Accounting', + accountID: null, }, HR: { enabled: true, name: 'HR', + accountID: null, }, }; - const smallResultList = [ + const smallResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: false, @@ -1138,7 +1258,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallSearchResultList = [ + const smallSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -1154,7 +1274,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList = [ + const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -1162,53 +1282,64 @@ describe('OptionsListUtils', () => { data: [], }, ]; - const largeTagsList = { + const largeTagsList: Record = { Engineering: { enabled: false, name: 'Engineering', + accountID: null, }, Medical: { enabled: true, name: 'Medical', + accountID: null, }, Accounting: { enabled: true, name: 'Accounting', + accountID: null, }, HR: { enabled: true, name: 'HR', + accountID: null, }, Food: { enabled: true, name: 'Food', + accountID: null, }, Traveling: { enabled: false, name: 'Traveling', + accountID: null, }, Cleaning: { enabled: true, name: 'Cleaning', + accountID: null, }, Software: { enabled: true, name: 'Software', + accountID: null, }, OfficeSupplies: { enabled: false, name: 'Office Supplies', + accountID: null, }, Taxes: { enabled: true, name: 'Taxes', + accountID: null, }, Benefits: { enabled: true, name: 'Benefits', + accountID: null, }, }; - const largeResultList = [ + const largeResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -1295,7 +1426,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeSearchResultList = [ + const largeSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -1318,7 +1449,7 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList = [ + const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ { title: '', shouldShow: true, @@ -2287,7 +2418,7 @@ describe('OptionsListUtils', () => { const emptySearch = ''; const wrongSearch = 'bla bla'; - const taxRatesWithDefault = { + const taxRatesWithDefault: TaxRatesWithDefault = { name: 'Tax', defaultExternalID: 'CODE1', defaultValue: '0%', @@ -2296,19 +2427,25 @@ describe('OptionsListUtils', () => { CODE2: { name: 'Tax rate 2', value: '3%', + code: 'CODE2', + modifiedName: 'Tax rate 2 (3%)', }, CODE3: { name: 'Tax option 3', value: '5%', + code: 'CODE3', + modifiedName: 'Tax option 3 (5%)', }, CODE1: { name: 'Tax exempt 1', value: '0%', + code: 'CODE1', + modifiedName: 'Tax exempt 1 (0%) • Default', }, }, }; - const resultList = [ + const resultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: false, @@ -2361,7 +2498,7 @@ describe('OptionsListUtils', () => { }, ]; - const searchResultList = [ + const searchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -2385,7 +2522,7 @@ describe('OptionsListUtils', () => { }, ]; - const wrongSearchResultList = [ + const wrongSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -2406,7 +2543,7 @@ describe('OptionsListUtils', () => { }); it('formatMemberForList()', () => { - const formattedMembers = _.map(PERSONAL_DETAILS, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); + const formattedMembers = Object.values(PERSONAL_DETAILS).map((personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); // We're only formatting items inside the array, so the order should be the same as the original PERSONAL_DETAILS array expect(formattedMembers[0].text).toBe('Mister Fantastic'); @@ -2417,9 +2554,9 @@ describe('OptionsListUtils', () => { expect(formattedMembers[0].isSelected).toBe(true); // And all the others to be unselected - expect(_.every(formattedMembers.slice(1), (personalDetail) => !personalDetail.isSelected)).toBe(true); + expect(formattedMembers.slice(1).every((personalDetail) => !personalDetail.isSelected)).toBe(true); // `isDisabled` is always false - expect(_.every(formattedMembers, (personalDetail) => !personalDetail.isDisabled)).toBe(true); + expect(formattedMembers.every((personalDetail) => !personalDetail.isDisabled)).toBe(true); }); }); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.ts similarity index 84% rename from tests/unit/ReportUtilsTest.js rename to tests/unit/ReportUtilsTest.ts index ffd5c9147dc0..2daefdc0657c 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.ts @@ -1,42 +1,45 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '../../src/CONST'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; +import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Be sure to include the mocked permissions library or else the beta tests won't work -jest.mock('../../src/libs/Permissions'); +jest.mock('@libs/Permissions'); const currentUserEmail = 'bjorn@vikings.net'; const currentUserAccountID = 5; -const participantsPersonalDetails = { - 1: { +const participantsPersonalDetails: PersonalDetailsList = { + '1': { accountID: 1, displayName: 'Ragnar Lothbrok', firstName: 'Ragnar', login: 'ragnar@vikings.net', }, - 2: { + '2': { accountID: 2, login: 'floki@vikings.net', displayName: 'floki@vikings.net', }, - 3: { + '3': { accountID: 3, displayName: 'Lagertha Lothbrok', firstName: 'Lagertha', login: 'lagertha@vikings.net', pronouns: 'She/her', }, - 4: { + '4': { accountID: 4, login: '+18332403627@expensify.sms', displayName: '(833) 240-3627', }, - 5: { + '5': { accountID: 5, displayName: 'Lagertha Lothbrok', firstName: 'Lagertha', @@ -44,20 +47,27 @@ const participantsPersonalDetails = { pronouns: 'She/her', }, }; -const policy = { - policyID: 1, + +const policy: Policy = { + id: '1', name: 'Vikings Policy', + role: 'user', + type: 'free', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, }; Onyx.init({keys: ONYXKEYS}); describe('ReportUtils', () => { beforeAll(() => { + const policyCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.POLICY, [policy], (current) => current.id); Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: participantsPersonalDetails, [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID}, [ONYXKEYS.COUNTRY_CODE]: 1, - [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + ...policyCollectionDataSet, }); return waitForBatchedUpdates(); }); @@ -107,6 +117,7 @@ describe('ReportUtils', () => { test('with displayName', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 1], }), ).toBe('Ragnar Lothbrok'); @@ -115,6 +126,7 @@ describe('ReportUtils', () => { test('no displayName', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 2], }), ).toBe('floki@vikings.net'); @@ -123,6 +135,7 @@ describe('ReportUtils', () => { test('SMS', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 4], }), ).toBe('(833) 240-3627'); @@ -132,6 +145,7 @@ describe('ReportUtils', () => { test('Group DM', () => { expect( ReportUtils.getReportName({ + reportID: '', participantAccountIDs: [currentUserAccountID, 1, 2, 3, 4], }), ).toBe('Ragnar, floki@vikings.net, Lagertha, (833) 240-3627'); @@ -139,6 +153,7 @@ describe('ReportUtils', () => { describe('Default Policy Room', () => { const baseAdminsRoom = { + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, reportName: '#admins', }; @@ -162,6 +177,7 @@ describe('ReportUtils', () => { describe('User-Created Policy Room', () => { const baseUserCreatedRoom = { + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, reportName: '#VikingsChat', }; @@ -188,8 +204,9 @@ describe('ReportUtils', () => { test('as member', () => { expect( ReportUtils.getReportName({ + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - policyID: policy.policyID, + policyID: policy.id, isOwnPolicyExpenseChat: true, ownerAccountID: 1, }), @@ -199,8 +216,9 @@ describe('ReportUtils', () => { test('as admin', () => { expect( ReportUtils.getReportName({ + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - policyID: policy.policyID, + policyID: policy.id, isOwnPolicyExpenseChat: false, ownerAccountID: 1, }), @@ -210,9 +228,10 @@ describe('ReportUtils', () => { describe('Archived', () => { const baseArchivedPolicyExpenseChat = { + reportID: '', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, ownerAccountID: 1, - policyID: policy.policyID, + policyID: policy.id, oldPolicyName: policy.name, statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, @@ -249,7 +268,7 @@ describe('ReportUtils', () => { describe('requiresAttentionFromCurrentUser', () => { it('returns false when there is no report', () => { - expect(ReportUtils.requiresAttentionFromCurrentUser()).toBe(false); + expect(ReportUtils.requiresAttentionFromCurrentUser(null)).toBe(false); }); it('returns false when the matched IOU report does not have an owner accountID', () => { const report = { @@ -324,7 +343,7 @@ describe('ReportUtils', () => { }); describe('getMoneyRequestOptions', () => { - const participantsAccountIDs = _.keys(participantsPersonalDetails); + const participantsAccountIDs = Object.keys(participantsPersonalDetails).map(Number); beforeAll(() => { Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { @@ -339,8 +358,8 @@ describe('ReportUtils', () => { describe('return empty iou options if', () => { it('participants aray contains excluded expensify iou emails', () => { - const allEmpty = _.every(CONST.EXPENSIFY_ACCOUNT_IDS, (accountID) => { - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions({}, {}, [currentUserAccountID, accountID]); + const allEmpty = CONST.EXPENSIFY_ACCOUNT_IDS.every((accountID) => { + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(null, null, [currentUserAccountID, accountID]); return moneyRequestOptions.length === 0; }); expect(allEmpty).toBe(true); @@ -351,7 +370,7 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); @@ -361,7 +380,7 @@ describe('ReportUtils', () => { chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: false, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); @@ -371,7 +390,7 @@ describe('ReportUtils', () => { type: CONST.REPORT.TYPE.IOU, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); @@ -382,7 +401,7 @@ describe('ReportUtils', () => { stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); @@ -392,7 +411,7 @@ describe('ReportUtils', () => { type: CONST.REPORT.TYPE.EXPENSE, statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); @@ -406,15 +425,20 @@ describe('ReportUtils', () => { parentReportID: '100', type: CONST.REPORT.TYPE.EXPENSE, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); }); }); it("it is a submitted report tied to user's own policy expense chat and the policy does not have Instant Submit frequency", () => { - const paidPolicy = { + const paidPolicy: Policy = { id: '3f54cca8', type: CONST.POLICY.TYPE.TEAM, + name: '', + role: 'user', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, }; Promise.all([ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy), @@ -440,17 +464,19 @@ describe('ReportUtils', () => { describe('return only iou split option if', () => { it('it is a chat room with more than one participant', () => { - const onlyHaveSplitOption = _.every( - [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, CONST.REPORT.CHAT_TYPE.POLICY_ROOM], - (chatType) => { - const report = { - ...LHNTestUtils.getFakeReport(), - chatType, - }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); - return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); - }, - ); + const onlyHaveSplitOption = [ + CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, + CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, + CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, + CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + ].every((chatType) => { + const report = { + ...LHNTestUtils.getFakeReport(), + chatType, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); + return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); + }); expect(onlyHaveSplitOption).toBe(true); }); @@ -459,7 +485,7 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); @@ -469,7 +495,7 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); @@ -480,7 +506,7 @@ describe('ReportUtils', () => { type: CONST.REPORT.TYPE.CHAT, participantsAccountIDs: [currentUserAccountID, ...participantsAccountIDs], }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs.map(Number)]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); @@ -498,7 +524,7 @@ describe('ReportUtils', () => { parentReportID: '102', type: CONST.REPORT.TYPE.EXPENSE, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); @@ -519,8 +545,14 @@ describe('ReportUtils', () => { }; const paidPolicy = { type: CONST.POLICY.TYPE.TEAM, - }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]], true); + id: '', + name: '', + role: 'user', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + } as const; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); }); }); @@ -532,7 +564,7 @@ describe('ReportUtils', () => { stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); @@ -544,17 +576,22 @@ describe('ReportUtils', () => { stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); it("it is a submitted expense report in user's own policyExpenseChat and the policy has Instant Submit frequency", () => { - const paidPolicy = { + const paidPolicy: Policy = { id: 'ef72dfeb', type: CONST.POLICY.TYPE.TEAM, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + name: '', + role: 'user', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, }; Promise.all([ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy), @@ -585,7 +622,7 @@ describe('ReportUtils', () => { chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: true, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, ...participantsAccountIDs]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); @@ -596,7 +633,7 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.CHAT, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); @@ -622,21 +659,21 @@ describe('ReportUtils', () => { describe('sortReportsByLastRead', () => { it('should filter out report without reportID & lastReadTime and sort lastReadTime in ascending order', () => { - const reports = [ - {reportID: 1, lastReadTime: '2023-07-08 07:15:44.030'}, - {reportID: 2, lastReadTime: null}, - {reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'}, - {reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, - {lastReadTime: '2023-07-09 07:15:44.030'}, - {reportID: 6}, - {}, + const reports: Array> = [ + {reportID: '1', lastReadTime: '2023-07-08 07:15:44.030'}, + {reportID: '2', lastReadTime: undefined}, + {reportID: '3', lastReadTime: '2023-07-06 07:15:44.030'}, + {reportID: '4', lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, + {lastReadTime: '2023-07-09 07:15:44.030'} as Report, + {reportID: '6'}, + null, ]; - const sortedReports = [ - {reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'}, - {reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, - {reportID: 1, lastReadTime: '2023-07-08 07:15:44.030'}, + const sortedReports: Array> = [ + {reportID: '3', lastReadTime: '2023-07-06 07:15:44.030'}, + {reportID: '4', lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU}, + {reportID: '1', lastReadTime: '2023-07-08 07:15:44.030'}, ]; - expect(ReportUtils.sortReportsByLastRead(reports)).toEqual(sortedReports); + expect(ReportUtils.sortReportsByLastRead(reports, null)).toEqual(sortedReports); }); }); @@ -656,7 +693,7 @@ describe('ReportUtils', () => { '', [{login: 'email1@test.com'}, {login: 'email2@test.com'}], NumberUtils.rand64(), - ); + ) as ReportAction; expect(ReportUtils.shouldDisableThread(reportAction, reportID)).toBeTruthy(); }); @@ -672,7 +709,7 @@ describe('ReportUtils', () => { }, ], childVisibleActionCount: 1, - }; + } as ReportAction; expect(ReportUtils.shouldDisableThread(reportAction, reportID)).toBeFalsy(); reportAction.childVisibleActionCount = 0; @@ -688,7 +725,7 @@ describe('ReportUtils', () => { .then(() => { const reportAction = { childVisibleActionCount: 1, - }; + } as ReportAction; expect(ReportUtils.shouldDisableThread(reportAction, reportID)).toBeFalsy(); reportAction.childVisibleActionCount = 0; @@ -700,20 +737,20 @@ describe('ReportUtils', () => { const reportAction = { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, whisperedToAccountIDs: [123456], - }; + } as ReportAction; expect(ReportUtils.shouldDisableThread(reportAction, reportID)).toBeTruthy(); }); it('should disable on thread first chat', () => { const reportAction = { childReportID: reportID, - }; + } as ReportAction; expect(ReportUtils.shouldDisableThread(reportAction, reportID)).toBeTruthy(); }); }); describe('getAllAncestorReportActions', () => { - const reports = [ + const reports: Report[] = [ {reportID: '1', lastReadTime: '2024-02-01 04:56:47.233', reportName: 'Report'}, {reportID: '2', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '1', parentReportID: '1', reportName: 'Report'}, {reportID: '3', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '2', parentReportID: '2', reportName: 'Report'}, @@ -721,24 +758,23 @@ describe('ReportUtils', () => { {reportID: '5', lastReadTime: '2024-02-01 04:56:47.233', parentReportActionID: '4', parentReportID: '4', reportName: 'Report'}, ]; - const reportActions = [ - {reportActionID: '1', created: '2024-02-01 04:42:22.965'}, - {reportActionID: '2', created: '2024-02-01 04:42:28.003'}, - {reportActionID: '3', created: '2024-02-01 04:42:31.742'}, - {reportActionID: '4', created: '2024-02-01 04:42:35.619'}, + const reportActions: ReportAction[] = [ + {reportActionID: '1', created: '2024-02-01 04:42:22.965', actionName: 'MARKEDREIMBURSED'}, + {reportActionID: '2', created: '2024-02-01 04:42:28.003', actionName: 'MARKEDREIMBURSED'}, + {reportActionID: '3', created: '2024-02-01 04:42:31.742', actionName: 'MARKEDREIMBURSED'}, + {reportActionID: '4', created: '2024-02-01 04:42:35.619', actionName: 'MARKEDREIMBURSED'}, ]; beforeAll(() => { + const reportCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT, reports, (report) => report.reportID); + const reportActionCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + reportActions.map((reportAction) => ({[reportAction.reportActionID]: reportAction})), + (actions) => Object.values(actions)[0].reportActionID, + ); Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${reports[0].reportID}`]: reports[0], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[1].reportID}`]: reports[1], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[2].reportID}`]: reports[2], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[3].reportID}`]: reports[3], - [`${ONYXKEYS.COLLECTION.REPORT}${reports[4].reportID}`]: reports[4], - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[0].reportID}`]: {[reportActions[0].reportActionID]: reportActions[0]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[1].reportID}`]: {[reportActions[1].reportActionID]: reportActions[1]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[2].reportID}`]: {[reportActions[2].reportActionID]: reportActions[2]}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reports[3].reportID}`]: {[reportActions[3].reportActionID]: reportActions[3]}, + ...reportCollectionDataSet, + ...reportActionCollectionDataSet, }); return waitForBatchedUpdates(); }); From 8fc529a1712ff73b65fa637c77e9d7bdede36c0d Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 28 Mar 2024 19:05:02 +0000 Subject: [PATCH 053/127] fix(typescript): jest tests and types --- src/libs/OptionsListUtils.ts | 2 +- tests/e2e/compare/output/console.ts | 4 ++-- tests/unit/OptionsListUtilsTest.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ab057a4c10ec..bf7ebf42f1a8 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2095,4 +2095,4 @@ export { getTaxRatesSection, }; -export type {MemberForList, CategorySection, CategoryTreeSection, GetOptions, PayeePersonalDetails, Category, Tag}; +export type {MemberForList, CategorySection, CategoryTreeSection, GetOptions, PayeePersonalDetails, Category}; diff --git a/tests/e2e/compare/output/console.ts b/tests/e2e/compare/output/console.ts index 3da0100b603f..77170e43f4a6 100644 --- a/tests/e2e/compare/output/console.ts +++ b/tests/e2e/compare/output/console.ts @@ -13,8 +13,8 @@ type Entry = { type Data = { significance: Entry[]; meaningless: Entry[]; - errors: string[]; - warnings: string[]; + errors?: string[]; + warnings?: string[]; }; const printRegularLine = (entry: Entry) => { diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index afa9b00ebc6a..72f40ea5d98b 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {SelectedTagOption} from '@components/TagPicker'; import CONST from '@src/CONST'; -import type {Tag} from '@src/libs/OptionsListUtils'; import * as OptionsListUtils from '@src/libs/OptionsListUtils'; import * as ReportUtils from '@src/libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -1205,7 +1205,7 @@ describe('OptionsListUtils', () => { name: 'Medical', }, ]; - const smallTagsList: Record = { + const smallTagsList: Record = { Engineering: { enabled: false, name: 'Engineering', @@ -1282,7 +1282,7 @@ describe('OptionsListUtils', () => { data: [], }, ]; - const largeTagsList: Record = { + const largeTagsList: Record = { Engineering: { enabled: false, name: 'Engineering', @@ -2190,7 +2190,7 @@ describe('OptionsListUtils', () => { }); it('sortTags', () => { - const createTagObjects = (names) => _.map(names, (name) => ({name, enabled: true})); + const createTagObjects = (names: string[]) => names.map((name) => ({name, enabled: true})); const unorderedTagNames = ['10bc', 'b', '0a', '1', '中国', 'b10', '!', '2', '0', '@', 'a1', 'a', '3', 'b1', '日本', '$', '20', '20a', '#', 'a20', 'c', '10']; const expectedOrderNames = ['!', '#', '$', '0', '0a', '1', '10', '10bc', '2', '20', '20a', '3', '@', 'a', 'a1', 'a20', 'b', 'b1', 'b10', 'c', '中国', '日本']; From b89c911cfdf6ca8f475ed6bb2ed4e922c41d2d02 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 19:55:57 +0100 Subject: [PATCH 054/127] Fix bug with User can access the background list using the arrow keys while the RHP is open --- .../BaseArrowKeyFocusManager.js} | 43 +++---------------- src/components/ArrowKeyFocusManager/index.js | 22 ++++++++++ .../ArrowKeyFocusManager/propTypes.js | 35 +++++++++++++++ 3 files changed, 64 insertions(+), 36 deletions(-) rename src/components/{ArrowKeyFocusManager.js => ArrowKeyFocusManager/BaseArrowKeyFocusManager.js} (68%) create mode 100644 src/components/ArrowKeyFocusManager/index.js create mode 100644 src/components/ArrowKeyFocusManager/propTypes.js diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager/BaseArrowKeyFocusManager.js similarity index 68% rename from src/components/ArrowKeyFocusManager.js rename to src/components/ArrowKeyFocusManager/BaseArrowKeyFocusManager.js index 19dc3a7ac614..6e37dbec04aa 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager/BaseArrowKeyFocusManager.js @@ -1,38 +1,9 @@ -import PropTypes from 'prop-types'; import {Component} from 'react'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import CONST from '@src/CONST'; +import {arrowKeyFocusManagerDefaultProps, arrowKeyFocusManagerPropTypes} from './propTypes'; -const propTypes = { - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /** Array of disabled indexes. */ - disabledIndexes: PropTypes.arrayOf(PropTypes.number), - - /** The current focused index. */ - focusedIndex: PropTypes.number.isRequired, - - /** The maximum index – provided so that the focus can be sent back to the beginning of the list when the end is reached. */ - maxIndex: PropTypes.number.isRequired, - - /** A callback executed when the focused input changes. */ - onFocusedIndexChanged: PropTypes.func.isRequired, - - /** If this value is true, then we exclude TextArea Node. */ - shouldExcludeTextAreaNodes: PropTypes.bool, - - /** If this value is true, then the arrow down callback would be triggered when the max index is exceeded */ - shouldResetIndexOnEndReached: PropTypes.bool, -}; - -const defaultProps = { - disabledIndexes: [], - shouldExcludeTextAreaNodes: true, - shouldResetIndexOnEndReached: true, -}; - -class ArrowKeyFocusManager extends Component { +class BaseArrowKeyFocusManager extends Component { componentDidMount() { const arrowUpConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_UP; const arrowDownConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN; @@ -77,7 +48,7 @@ class ArrowKeyFocusManager extends Component { } onArrowUpKey() { - if (this.props.maxIndex < 0) { + if (this.props.maxIndex < 0 || !this.props.isFocused) { return; } @@ -96,7 +67,7 @@ class ArrowKeyFocusManager extends Component { } onArrowDownKey() { - if (this.props.maxIndex < 0) { + if (this.props.maxIndex < 0 || !this.props.isFocused) { return; } @@ -119,7 +90,7 @@ class ArrowKeyFocusManager extends Component { } } -ArrowKeyFocusManager.propTypes = propTypes; -ArrowKeyFocusManager.defaultProps = defaultProps; +BaseArrowKeyFocusManager.propTypes = arrowKeyFocusManagerPropTypes; +BaseArrowKeyFocusManager.defaultProps = arrowKeyFocusManagerDefaultProps; -export default ArrowKeyFocusManager; +export default BaseArrowKeyFocusManager; diff --git a/src/components/ArrowKeyFocusManager/index.js b/src/components/ArrowKeyFocusManager/index.js new file mode 100644 index 000000000000..0fde2d357c1a --- /dev/null +++ b/src/components/ArrowKeyFocusManager/index.js @@ -0,0 +1,22 @@ +import {useIsFocused} from '@react-navigation/native'; +import React from 'react'; +import BaseArrowKeyFocusManager from './BaseArrowKeyFocusManager'; +import {arrowKeyFocusManagerDefaultProps, arrowKeyFocusManagerPropTypes} from './propTypes'; + +function ArrowKeyFocusManager(props) { + const isFocused = useIsFocused(); + + return ( + + ); +} + +ArrowKeyFocusManager.propTypes = arrowKeyFocusManagerPropTypes; +ArrowKeyFocusManager.defaultProps = arrowKeyFocusManagerDefaultProps; +ArrowKeyFocusManager.displayName = 'ArrowKeyFocusManager'; + +export default ArrowKeyFocusManager; diff --git a/src/components/ArrowKeyFocusManager/propTypes.js b/src/components/ArrowKeyFocusManager/propTypes.js new file mode 100644 index 000000000000..3f3154f745c5 --- /dev/null +++ b/src/components/ArrowKeyFocusManager/propTypes.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; + +const arrowKeyFocusManagerPropTypes = { + /** Children to render. */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /** Array of disabled indexes. */ + disabledIndexes: PropTypes.arrayOf(PropTypes.number), + + /** The current focused index. */ + focusedIndex: PropTypes.number.isRequired, + + /** The maximum index – provided so that the focus can be sent back to the beginning of the list when the end is reached. */ + maxIndex: PropTypes.number.isRequired, + + /** A callback executed when the focused input changes. */ + onFocusedIndexChanged: PropTypes.func.isRequired, + + /** If this value is true, then we exclude TextArea Node. */ + shouldExcludeTextAreaNodes: PropTypes.bool, + + /** If this value is true, then the arrow down callback would be triggered when the max index is exceeded */ + shouldResetIndexOnEndReached: PropTypes.bool, + + /** Whether navigation is focused */ + isFocused: PropTypes.bool, +}; + +const arrowKeyFocusManagerDefaultProps = { + disabledIndexes: [], + shouldExcludeTextAreaNodes: true, + shouldResetIndexOnEndReached: true, +}; + +export {arrowKeyFocusManagerDefaultProps, arrowKeyFocusManagerPropTypes}; From b771515740111eb72e0de2a797663b68eba6c3c9 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 20:10:11 +0100 Subject: [PATCH 055/127] Refactor ArrowKeyFocusManager --- ...ocusManager.js => ArrowKeyFocusManager.js} | 56 +++++++++++++++++-- src/components/ArrowKeyFocusManager/index.js | 22 -------- .../ArrowKeyFocusManager/propTypes.js | 35 ------------ 3 files changed, 51 insertions(+), 62 deletions(-) rename src/components/{ArrowKeyFocusManager/BaseArrowKeyFocusManager.js => ArrowKeyFocusManager.js} (63%) delete mode 100644 src/components/ArrowKeyFocusManager/index.js delete mode 100644 src/components/ArrowKeyFocusManager/propTypes.js diff --git a/src/components/ArrowKeyFocusManager/BaseArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js similarity index 63% rename from src/components/ArrowKeyFocusManager/BaseArrowKeyFocusManager.js rename to src/components/ArrowKeyFocusManager.js index 6e37dbec04aa..c8bdf1845f2b 100644 --- a/src/components/ArrowKeyFocusManager/BaseArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -1,7 +1,40 @@ -import {Component} from 'react'; +import {useIsFocused} from '@react-navigation/native'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import CONST from '@src/CONST'; -import {arrowKeyFocusManagerDefaultProps, arrowKeyFocusManagerPropTypes} from './propTypes'; + +const propTypes = { + /** Children to render. */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /** Array of disabled indexes. */ + disabledIndexes: PropTypes.arrayOf(PropTypes.number), + + /** The current focused index. */ + focusedIndex: PropTypes.number.isRequired, + + /** The maximum index – provided so that the focus can be sent back to the beginning of the list when the end is reached. */ + maxIndex: PropTypes.number.isRequired, + + /** A callback executed when the focused input changes. */ + onFocusedIndexChanged: PropTypes.func.isRequired, + + /** Whether navigation is focused */ + isFocused: PropTypes.bool.isRequired, + + /** If this value is true, then we exclude TextArea Node. */ + shouldExcludeTextAreaNodes: PropTypes.bool, + + /** If this value is true, then the arrow down callback would be triggered when the max index is exceeded */ + shouldResetIndexOnEndReached: PropTypes.bool, +}; + +const defaultProps = { + disabledIndexes: [], + shouldExcludeTextAreaNodes: true, + shouldResetIndexOnEndReached: true, +}; class BaseArrowKeyFocusManager extends Component { componentDidMount() { @@ -90,7 +123,20 @@ class BaseArrowKeyFocusManager extends Component { } } -BaseArrowKeyFocusManager.propTypes = arrowKeyFocusManagerPropTypes; -BaseArrowKeyFocusManager.defaultProps = arrowKeyFocusManagerDefaultProps; +function ArrowKeyFocusManager(props) { + const isFocused = useIsFocused(); + + return ( + + ); +} + +BaseArrowKeyFocusManager.propTypes = propTypes; +BaseArrowKeyFocusManager.defaultProps = defaultProps; +ArrowKeyFocusManager.displayName = 'ArrowKeyFocusManager'; -export default BaseArrowKeyFocusManager; +export default ArrowKeyFocusManager; diff --git a/src/components/ArrowKeyFocusManager/index.js b/src/components/ArrowKeyFocusManager/index.js deleted file mode 100644 index 0fde2d357c1a..000000000000 --- a/src/components/ArrowKeyFocusManager/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import {useIsFocused} from '@react-navigation/native'; -import React from 'react'; -import BaseArrowKeyFocusManager from './BaseArrowKeyFocusManager'; -import {arrowKeyFocusManagerDefaultProps, arrowKeyFocusManagerPropTypes} from './propTypes'; - -function ArrowKeyFocusManager(props) { - const isFocused = useIsFocused(); - - return ( - - ); -} - -ArrowKeyFocusManager.propTypes = arrowKeyFocusManagerPropTypes; -ArrowKeyFocusManager.defaultProps = arrowKeyFocusManagerDefaultProps; -ArrowKeyFocusManager.displayName = 'ArrowKeyFocusManager'; - -export default ArrowKeyFocusManager; diff --git a/src/components/ArrowKeyFocusManager/propTypes.js b/src/components/ArrowKeyFocusManager/propTypes.js deleted file mode 100644 index 3f3154f745c5..000000000000 --- a/src/components/ArrowKeyFocusManager/propTypes.js +++ /dev/null @@ -1,35 +0,0 @@ -import PropTypes from 'prop-types'; - -const arrowKeyFocusManagerPropTypes = { - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /** Array of disabled indexes. */ - disabledIndexes: PropTypes.arrayOf(PropTypes.number), - - /** The current focused index. */ - focusedIndex: PropTypes.number.isRequired, - - /** The maximum index – provided so that the focus can be sent back to the beginning of the list when the end is reached. */ - maxIndex: PropTypes.number.isRequired, - - /** A callback executed when the focused input changes. */ - onFocusedIndexChanged: PropTypes.func.isRequired, - - /** If this value is true, then we exclude TextArea Node. */ - shouldExcludeTextAreaNodes: PropTypes.bool, - - /** If this value is true, then the arrow down callback would be triggered when the max index is exceeded */ - shouldResetIndexOnEndReached: PropTypes.bool, - - /** Whether navigation is focused */ - isFocused: PropTypes.bool, -}; - -const arrowKeyFocusManagerDefaultProps = { - disabledIndexes: [], - shouldExcludeTextAreaNodes: true, - shouldResetIndexOnEndReached: true, -}; - -export {arrowKeyFocusManagerDefaultProps, arrowKeyFocusManagerPropTypes}; From 108f04f9effc6811b53cd52b01f0d0a6db4e2bc9 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 21:39:35 +0100 Subject: [PATCH 056/127] Update propTypes for ArrowKeyFocusManager --- src/components/ArrowKeyFocusManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index c8bdf1845f2b..2532e52156df 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -17,12 +17,12 @@ const propTypes = { /** The maximum index – provided so that the focus can be sent back to the beginning of the list when the end is reached. */ maxIndex: PropTypes.number.isRequired, - /** A callback executed when the focused input changes. */ - onFocusedIndexChanged: PropTypes.func.isRequired, - /** Whether navigation is focused */ isFocused: PropTypes.bool.isRequired, + /** A callback executed when the focused input changes. */ + onFocusedIndexChanged: PropTypes.func.isRequired, + /** If this value is true, then we exclude TextArea Node. */ shouldExcludeTextAreaNodes: PropTypes.bool, From e3f803b5998dad4bc24e009cae014f329514b838 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 21:54:50 +0100 Subject: [PATCH 057/127] Update propTypes for ArrowKeyFocusManager x2 --- src/components/ArrowKeyFocusManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index 2532e52156df..3dccdb92bd0d 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -18,7 +18,7 @@ const propTypes = { maxIndex: PropTypes.number.isRequired, /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, + isFocused: PropTypes.bool, /** A callback executed when the focused input changes. */ onFocusedIndexChanged: PropTypes.func.isRequired, @@ -34,6 +34,7 @@ const defaultProps = { disabledIndexes: [], shouldExcludeTextAreaNodes: true, shouldResetIndexOnEndReached: true, + isFocused: false, }; class BaseArrowKeyFocusManager extends Component { From 9efe7126b4f47606b3c587ab63a56d6bc16b2257 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 22:17:20 +0100 Subject: [PATCH 058/127] Update perf-tests --- src/components/ArrowKeyFocusManager.js | 1 - tests/perf-test/SearchPage.perf-test.tsx | 5 +---- tests/perf-test/SignInPage.perf-test.tsx | 5 +---- tests/utils/LHNTestUtils.tsx | 8 +------- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index 3dccdb92bd0d..c78d97ad68fe 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -34,7 +34,6 @@ const defaultProps = { disabledIndexes: [], shouldExcludeTextAreaNodes: true, shouldResetIndexOnEndReached: true, - isFocused: false, }; class BaseArrowKeyFocusManager extends Component { diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx index 5f2dd3800e0f..489c282898cd 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/SearchPage.perf-test.tsx @@ -43,15 +43,12 @@ jest.mock('@src/libs/API', () => ({ jest.mock('@src/libs/Navigation/Navigation'); -const mockedNavigate = jest.fn(); jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useFocusEffect: jest.fn(), - useIsFocused: () => ({ - navigate: mockedNavigate, - }), + useIsFocused: () => true, useRoute: () => jest.fn(), useNavigation: () => ({ navigate: jest.fn(), diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx index e3e2c20ae72a..651d85a70e58 100644 --- a/tests/perf-test/SignInPage.perf-test.tsx +++ b/tests/perf-test/SignInPage.perf-test.tsx @@ -26,15 +26,12 @@ jest.mock('../../src/libs/API', () => ({ read: jest.fn(), })); -const mockedNavigate = jest.fn(); jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useFocusEffect: jest.fn(), - useIsFocused: () => ({ - navigate: mockedNavigate, - }), + useIsFocused: () => true, useRoute: () => jest.fn(), useNavigation: () => ({ navigate: jest.fn(), diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index f6eee590313b..58c765b5c98e 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type {NavigationProp} from '@react-navigation/core/src/types'; import type * as Navigation from '@react-navigation/native'; -import type {ParamListBase} from '@react-navigation/routers'; import {render} from '@testing-library/react-native'; import type {ReactElement} from 'react'; import React from 'react'; @@ -33,17 +31,13 @@ type MockedSidebarLinksProps = { currentReportID?: string; }; -// we have to mock `useIsFocused` because it's used in the SidebarLinks component -const mockedNavigate: jest.MockedFn['navigate']> = jest.fn(); jest.mock('@react-navigation/native', (): typeof Navigation => { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useRoute: jest.fn(), useFocusEffect: jest.fn(), - useIsFocused: () => ({ - navigate: mockedNavigate, - }), + useIsFocused: () => true, useNavigation: () => ({ navigate: jest.fn(), addListener: jest.fn(), From 9eba416539f7960f8eabbc0f196af620499aff5c Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 22:18:03 +0100 Subject: [PATCH 059/127] Update perf-tests x2 --- src/components/ArrowKeyFocusManager.js | 2 +- tests/perf-test/ReportActionCompose.perf-test.tsx | 4 +--- tests/perf-test/ReportScreen.perf-test.tsx | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index c78d97ad68fe..2532e52156df 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -18,7 +18,7 @@ const propTypes = { maxIndex: PropTypes.number.isRequired, /** Whether navigation is focused */ - isFocused: PropTypes.bool, + isFocused: PropTypes.bool.isRequired, /** A callback executed when the focused input changes. */ onFocusedIndexChanged: PropTypes.func.isRequired, diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 8d6cb1ac7e57..7d3846e4e29a 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -38,9 +38,7 @@ jest.mock('@react-navigation/native', () => { navigate: jest.fn(), addListener: () => jest.fn(), }), - useIsFocused: () => ({ - navigate: jest.fn(), - }), + useIsFocused: () => true, } as typeof Navigation; }); diff --git a/tests/perf-test/ReportScreen.perf-test.tsx b/tests/perf-test/ReportScreen.perf-test.tsx index ff3d1473c662..b55f4b9ccb93 100644 --- a/tests/perf-test/ReportScreen.perf-test.tsx +++ b/tests/perf-test/ReportScreen.perf-test.tsx @@ -81,15 +81,12 @@ jest.mock('@src/hooks/usePermissions.ts'); jest.mock('@src/libs/Navigation/Navigation'); -const mockedNavigate = jest.fn(); jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useFocusEffect: jest.fn(), - useIsFocused: () => ({ - navigate: mockedNavigate, - }), + useIsFocused: () => true, useRoute: () => jest.fn(), useNavigation: () => ({ navigate: jest.fn(), From defd7311fdb2bc37ce93981dc97ac00eef344db7 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 22:24:05 +0100 Subject: [PATCH 060/127] Update perf-tests x3 --- src/components/SelectionList/BaseListItem.tsx | 138 ++++++++---------- .../SelectionList/TableListItem.tsx | 12 -- src/pages/workspace/WorkspaceMembersPage.tsx | 1 + .../ReportActionCompose.perf-test.tsx | 2 +- tests/perf-test/SignInPage.perf-test.tsx | 2 +- 5 files changed, 67 insertions(+), 88 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 42fdc7dc575e..67de9153d0d6 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -58,85 +58,75 @@ function BaseListItem({ }; return ( - onDismissError(item)} - pendingAction={pendingAction} - errors={errors} - errorRowStyles={styles.ph5} - style={containerStyle} + onSelectRow(item)} + disabled={isDisabled} + accessibilityLabel={item.text ?? ''} + hoverDimmingValue={1} + hoverStyle={!item.isDisabled && !item.isSelected && styles.hoveredComponentBG} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + nativeID={keyForList ?? ''} + style={pressableStyle} > - onSelectRow(item)} - disabled={isDisabled} - accessibilityLabel={item.text ?? ''} - role={CONST.ROLE.BUTTON} - hoverDimmingValue={1} - hoverStyle={!item.isDisabled && !item.isSelected && styles.hoveredComponentBG} - dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} - onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} - nativeID={keyForList ?? ''} - style={pressableStyle} - > - - {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && ( - - - {item.isSelected && ( - - )} - - - )} - - {typeof children === 'function' ? children(hovered) : children} - - {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.RIGHT && ( - - - - )} - - {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( - - + + {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && ( + + + {item.isSelected && ( - + )} + + + )} + + {typeof children === 'function' ? children(hovered) : children} + + {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.RIGHT && ( + + + + )} + + {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( + + + - )} - {rightHandSideComponentRender()} - - {FooterComponent} - - +
+ )} + {rightHandSideComponentRender()} + + {FooterComponent} + ); } diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx index b7c3ed549d82..1fb996bc13b9 100644 --- a/src/components/SelectionList/TableListItem.tsx +++ b/src/components/SelectionList/TableListItem.tsx @@ -31,10 +31,6 @@ function TableListItem({ return ( {!!item.alternateText && ( + // { navigate: jest.fn(), addListener: () => jest.fn(), }), - useIsFocused: () => true, + useIsFocused: () => true, } as typeof Navigation; }); diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx index 651d85a70e58..dc93b0d81059 100644 --- a/tests/perf-test/SignInPage.perf-test.tsx +++ b/tests/perf-test/SignInPage.perf-test.tsx @@ -31,7 +31,7 @@ jest.mock('@react-navigation/native', () => { return { ...actualNav, useFocusEffect: jest.fn(), - useIsFocused: () => true, + useIsFocused: () => true, useRoute: () => jest.fn(), useNavigation: () => ({ navigate: jest.fn(), From aeb387f1c930698143fc5ade272f349422df68bf Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 22:27:43 +0100 Subject: [PATCH 061/127] Revert some changes --- src/components/SelectionList/BaseListItem.tsx | 138 ++++++++++-------- .../SelectionList/TableListItem.tsx | 12 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 1 - 3 files changed, 86 insertions(+), 65 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 67de9153d0d6..6acb9284db6b 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -58,75 +58,85 @@ function BaseListItem({ }; return ( - onSelectRow(item)} - disabled={isDisabled} - accessibilityLabel={item.text ?? ''} - hoverDimmingValue={1} - hoverStyle={!item.isDisabled && !item.isSelected && styles.hoveredComponentBG} - onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} - nativeID={keyForList ?? ''} - style={pressableStyle} + onDismissError(item)} + pendingAction={pendingAction} + errors={errors} + errorRowStyles={styles.ph5} + style={containerStyle} > - - {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && ( - - - {item.isSelected && ( - - )} - - - )} - - {typeof children === 'function' ? children(hovered) : children} + onSelectRow(item)} + disabled={isDisabled} + accessibilityLabel={item.text ?? ''} + role={CONST.ROLE.BUTTON} + hoverDimmingValue={1} + hoverStyle={!item.isSelected && styles.hoveredComponentBG} + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + nativeID={keyForList ?? ''} + style={pressableStyle} + > + + {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && ( + + + {item.isSelected && ( + + )} + + + )} - {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.RIGHT && ( - - - - )} + {typeof children === 'function' ? children(hovered) : children} - {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( - - - + + + )} + + {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( + + + + - - )} - {rightHandSideComponentRender()} - - {FooterComponent} - + )} + {rightHandSideComponentRender()} + + {FooterComponent} + + ); } diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx index 1fb996bc13b9..b7c3ed549d82 100644 --- a/src/components/SelectionList/TableListItem.tsx +++ b/src/components/SelectionList/TableListItem.tsx @@ -31,6 +31,10 @@ function TableListItem({ return ( {!!item.alternateText && ( - // Date: Sat, 30 Mar 2024 22:28:52 +0100 Subject: [PATCH 062/127] Revert some changes x2 --- src/components/SelectionList/BaseListItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 6acb9284db6b..42fdc7dc575e 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -73,7 +73,7 @@ function BaseListItem({ accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} - hoverStyle={!item.isSelected && styles.hoveredComponentBG} + hoverStyle={!item.isDisabled && !item.isSelected && styles.hoveredComponentBG} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} nativeID={keyForList ?? ''} @@ -87,7 +87,7 @@ function BaseListItem({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing disabled={isDisabled || item.isDisabledCheckbox} onPress={handleCheckboxPress} - style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled]} + style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3]} > {item.isSelected && ( From 13affe7836a81c1b3eef58f1e677b7f4a545eee1 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 22:51:00 +0100 Subject: [PATCH 063/127] Update perf-tests --- tests/perf-test/SidebarUtils.perf-test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8566abb97c7f..df99f9ce3758 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -2,6 +2,7 @@ import {rand} from '@ngneat/falso'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; +import type Navigation from '@libs/Navigation/Navigation'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -73,6 +74,18 @@ const allReportActions = Object.fromEntries( const currentReportId = '1'; const transactionViolations = {} as OnyxCollection; +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + useIsFocused: () => true, + } as typeof Navigation; +}); + describe('SidebarUtils', () => { beforeAll(() => { Onyx.init({ From aae8ff4b45a59e811e25a384fe472d7080931c26 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 23:43:37 +0100 Subject: [PATCH 064/127] Update perf-tests x2 --- tests/perf-test/OptionsSelector.perf-test.tsx | 13 +++++++++++++ tests/perf-test/SidebarUtils.perf-test.ts | 12 ------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/perf-test/OptionsSelector.perf-test.tsx b/tests/perf-test/OptionsSelector.perf-test.tsx index 44dc4ac6c317..fe234dda1e19 100644 --- a/tests/perf-test/OptionsSelector.perf-test.tsx +++ b/tests/perf-test/OptionsSelector.perf-test.tsx @@ -5,6 +5,7 @@ import type {ComponentType} from 'react'; import {measurePerformance} from 'reassure'; import type {WithLocalizeProps} from '@components/withLocalize'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; +import type Navigation from '@libs/Navigation/Navigation'; import OptionsSelector from '@src/components/OptionsSelector'; import variables from '@src/styles/variables'; @@ -38,6 +39,18 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType return WithNavigationFocus; }); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + useIsFocused: () => true, + } as typeof Navigation; +}); + type GenerateSectionsProps = Array<{numberOfItems: number; shouldShow?: boolean}>; const generateSections = (sections: GenerateSectionsProps) => diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index df99f9ce3758..e9ab30495c87 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -74,18 +74,6 @@ const allReportActions = Object.fromEntries( const currentReportId = '1'; const transactionViolations = {} as OnyxCollection; -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - return { - ...actualNav, - useNavigation: () => ({ - navigate: jest.fn(), - addListener: () => jest.fn(), - }), - useIsFocused: () => true, - } as typeof Navigation; -}); - describe('SidebarUtils', () => { beforeAll(() => { Onyx.init({ From 917cf001c037c2f4ea5ccf9432d94ac5c6c68f65 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 30 Mar 2024 23:44:12 +0100 Subject: [PATCH 065/127] Update perf-tests x3 --- tests/perf-test/SidebarUtils.perf-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index e9ab30495c87..8566abb97c7f 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -2,7 +2,6 @@ import {rand} from '@ngneat/falso'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; -import type Navigation from '@libs/Navigation/Navigation'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From f4f7b19e4bd8f45dc472056cee99f309928fa79f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sun, 31 Mar 2024 01:14:03 +0100 Subject: [PATCH 066/127] Fix bug with blue border --- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 ++ src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx | 2 ++ src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 ++ src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index c5e79effe276..c4975ab340be 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -23,6 +23,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@libs/actions/Policy'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -316,6 +317,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat sections={[{data: categoryList, isDisabled: false}]} onCheckboxPress={toggleCategory} onSelectRow={navigateToCategorySettings} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectAll={toggleAllCategories} showScrollIndicator ListItem={TableListItem} diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index a5356a8fd05a..ac84455b0e10 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -21,6 +21,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; @@ -308,6 +309,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) onDismissError={dismissError} showScrollIndicator ListItem={TableListItem} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 53376c05878f..118f32549386 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -23,6 +23,7 @@ import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -315,6 +316,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { showScrollIndicator ListItem={TableListItem} customListHeader={getCustomListHeader()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} onDismissError={(item) => Policy.clearPolicyTagErrors(route.params.policyID, item.value)} /> diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 4f8782dcdf3f..ce9bbf1e9ae4 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -21,6 +21,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {openPolicyTaxesPage} from '@libs/actions/Policy'; import {clearTaxRateError, deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -276,6 +277,7 @@ function WorkspaceTaxesPage({ showScrollIndicator ListItem={TableListItem} customListHeader={getCustomListHeader()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} /> From 025be6bf577f86decae8e0d0dd5058f5e680da8f Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 1 Apr 2024 10:32:54 +0700 Subject: [PATCH 067/127] fix conflict --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8b40e7ef5d37..664854a2f2a7 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -514,7 +514,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString); } - }, [policyTagLists, transaction, policyTags, isTagRequired, canUseViolations]); + }, [policyTagLists, transaction, policyTags, canUseViolations]); /** * @param {Object} option From d7d5a878e3c050a1fa638da8bf25bf759fdf6f22 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 1 Apr 2024 23:58:40 +0800 Subject: [PATCH 068/127] fix selection bug --- src/libs/focusComposerWithDelay/index.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/libs/focusComposerWithDelay/index.ts b/src/libs/focusComposerWithDelay/index.ts index 6e3f6d1828e9..cbd81b884d12 100644 --- a/src/libs/focusComposerWithDelay/index.ts +++ b/src/libs/focusComposerWithDelay/index.ts @@ -27,17 +27,15 @@ function focusComposerWithDelay(textInput: InputType | null): FocusComposerWithD } return; } - ComposerFocusManager.isReadyToFocus() - .then(isWindowReadyToFocus) - .then(() => { - if (!textInput) { - return; - } - textInput.focus(); - if (forcedSelectionRange) { - setTextInputSelection(textInput, forcedSelectionRange); - } - }); + Promise.all([ComposerFocusManager.isReadyToFocus(), isWindowReadyToFocus()]).then(() => { + if (!textInput) { + return; + } + textInput.focus(); + if (forcedSelectionRange) { + setTextInputSelection(textInput, forcedSelectionRange); + } + }); }; } From 50bc061e7bb511314276d644ed01df50a7ef6d3c Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 2 Apr 2024 12:43:06 +0200 Subject: [PATCH 069/127] review fixes --- src/components/SelectionList/BaseListItem.tsx | 2 +- .../SelectionList/BaseSelectionList.tsx | 3 +- src/components/SelectionList/types.ts | 6 +-- src/components/WorkspaceSwitcherButton.tsx | 6 ++- src/libs/OptionsListUtils.ts | 3 +- src/pages/WorkspaceSwitcherPage.tsx | 5 +-- .../ShareLogList/BaseShareLogList.tsx | 42 ++++++++----------- 7 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 9f2fa68111cf..3fee43dbb425 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -132,7 +132,7 @@ function BaseListItem({ )} - {!item.isSelected && item.brickRoadIndicator && [CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR].includes(item.brickRoadIndicator) && ( + {!item.isSelected && item.brickRoadIndicator && ( ( textInputRef, textInputIconLeft, sectionTitleStyles, - turnOffEnterDisabling, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -195,7 +194,7 @@ function BaseSelectionList( ); // Disable `Enter` shortcut if the active element is a button or checkbox - const disableEnterShortcut = !turnOffEnterDisabling && activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); + const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); /** * Scrolls to the desired item index in the section list diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index c35623e1c63e..4a2c82e54ce9 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -144,7 +144,6 @@ type BaseListItemProps = CommonListItemProps & { pendingAction?: PendingAction | null; FooterComponent?: ReactElement; children?: ReactElement | ((hovered: boolean) => ReactElement); - brickRoadIndicator?: BrickRoad | '' | null; }; type UserListItemProps = ListItemProps & { @@ -217,7 +216,7 @@ type BaseSelectionListProps = Partial & { textInputMaxLength?: number; /** Icon to display on the left side of TextInput */ - textInputIconLeft?: IconAsset | null; + textInputIconLeft?: IconAsset; /** Callback to fire when the text input changes */ onChangeText?: (text: string) => void; @@ -314,9 +313,6 @@ type BaseSelectionListProps = Partial & { /** Styles for the section title */ sectionTitleStyles?: StyleProp; - - /** Decides if selecting with Enter should be disabled */ - turnOffEnterDisabling?: boolean; }; type SelectionListHandle = { diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index a94f54682c85..ffbdb55b7dce 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -1,4 +1,5 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useRef} from 'react'; +import type {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; @@ -22,6 +23,7 @@ type WorkspaceSwitcherButtonProps = WorkspaceSwitcherButtonOnyxProps; function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const {translate} = useLocalize(); const theme = useTheme(); + const pressableRef = useRef(); const {source, name, type} = useMemo(() => { if (!policy) { @@ -40,10 +42,12 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { interceptAnonymousUser(() => { + pressableRef.current?.blur(); Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); }) } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 48d4a316e2ff..00e327c5bd27 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1095,7 +1095,6 @@ function getTagsOptions(tags: Array>, select return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); - const selectedOptionsNames = selectedOptions?.map(({name}) => name); return { text: cleanedName, @@ -1103,7 +1102,7 @@ function getTagsOptions(tags: Array>, select searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, - isSelected: selectedOptionsNames?.includes(tag.name), + isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), }; }); } diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 115e1ee25960..80b53c9a7833 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -243,12 +243,9 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { onSelectRow={selectPolicy} shouldPreventDefaultFocusOnSelectRow headerMessage={headerMessage} - shouldShowTooltips - autoFocus={false} containerStyle={[styles.pt0, styles.mt0]} textInputIconLeft={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? MagnifyingGlass : undefined} - initiallyFocusedOptionKey={activeWorkspaceID ?? undefined} - turnOffEnterDisabling + initiallyFocusedOptionKey={activeWorkspaceID} /> ) : ( diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 8412e317712b..04375609bc6f 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -118,30 +118,24 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis testID={BaseShareLogList.displayName} includeSafeAreaPaddingBottom={false} > - {({safeAreaPaddingBottomStyle}) => ( - <> - Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} - /> - - - - - )} + Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} + /> + + + ); } From b55a0858ff5a189777b7f73b41181545c8611d04 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 2 Apr 2024 13:12:17 +0200 Subject: [PATCH 070/127] Remove autofocus --- src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 04375609bc6f..5e5478034c65 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -133,7 +133,6 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis isLoadingNewOptions={!isOptionsDataReady} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputHint={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} - autoFocus /> From efce0824765ca52189b1cb23194ba31bc933b510 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Tue, 2 Apr 2024 12:47:28 +0100 Subject: [PATCH 071/127] fix comments --- src/types/onyx/Card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index d9df6513d89f..11cc8dc26b4c 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -9,13 +9,13 @@ type Card = { availableSpend: number; domainName: string; lastFourPAN?: string; - isVirtual: boolean; + isVirtual: boolean; // Deprecating, use nameValuePairs.isVirtual fraud: ValueOf; errors?: OnyxCommon.Errors; isLoading?: boolean; nameValuePairs?: { limitType?: ValueOf; - cardTitle?: string; // used only for virtual limit cards + cardTitle?: string; // Used only for admin-issued virtual cards issuedBy?: number; hasCustomUnapprovedExpenseLimit?: boolean; unapprovedExpenseLimit?: number; From c8a23e40005a77bbb51c6e637a7be6f13e4f0637 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 2 Apr 2024 15:13:19 +0200 Subject: [PATCH 072/127] Fix isSelected state for dropdown --- src/pages/EditReportFieldDropdownPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index b82589c03bf9..575f77e88b64 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -73,6 +73,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, keyForList: option, searchText: option, tooltipText: option, + isSelected: option === fieldValue, })), }); } else { From cb1cc782842e6174ab1d0fd4e7882087c52d2dda Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:12:54 +0530 Subject: [PATCH 073/127] Hide notification badge on iOS app reinstall --- ios/NewExpensify/AppDelegate.mm | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index f5ddba46f5f1..30ea7e6d291e 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -22,7 +22,7 @@ - (BOOL)application:(UIApplication *)application // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; - + // Configure firebase [FIRApp configure]; @@ -34,7 +34,7 @@ - (BOOL)application:(UIApplication *)application [RCTBootSplash initWithStoryboard:@"BootSplash" rootView:(RCTRootView *)self.window.rootViewController.view]; // <- initialization using the storyboard file name - + // Define UNUserNotificationCenter UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; @@ -44,7 +44,12 @@ - (BOOL)application:(UIApplication *)application // stopped by a native module in the JS so we can measure total time starting // in the native layer and ending in the JS layer. [RCTStartupTimer start]; - + + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"isFirstRunComplete"]) { + [UIApplication sharedApplication].applicationIconBadgeNumber = 0; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isFirstRunComplete"]; + } + return YES; } From 9e775d5bffad543e230ad3266fe166aa7376ef6c Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:04:57 +0530 Subject: [PATCH 074/127] Update AppDelegate.mm --- ios/NewExpensify/AppDelegate.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index 30ea7e6d291e..f4f7f3bc8dbc 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -22,7 +22,7 @@ - (BOOL)application:(UIApplication *)application // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; - + // Configure firebase [FIRApp configure]; @@ -34,7 +34,7 @@ - (BOOL)application:(UIApplication *)application [RCTBootSplash initWithStoryboard:@"BootSplash" rootView:(RCTRootView *)self.window.rootViewController.view]; // <- initialization using the storyboard file name - + // Define UNUserNotificationCenter UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; From dcaca33047dd8270af1e09eb28274fe69d8f67b8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Wed, 3 Apr 2024 09:46:02 +0200 Subject: [PATCH 075/127] Optimizing getPersonalDetailByEmail using key-value pair object mapping --- src/libs/PersonalDetailsUtils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index c9ea65781117..081d1139bf5d 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -17,11 +17,18 @@ type FirstAndLastName = { let personalDetails: Array = []; let allPersonalDetails: OnyxEntry = {}; +let emailToPersonalDetailsCache: Record = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => { personalDetails = Object.values(val ?? {}); allPersonalDetails = val; + emailToPersonalDetailsCache = personalDetails.reduce((acc: Record, detail) => { + if (detail?.login) { + acc[detail.login.toLowerCase()] = detail; + } + return acc; + }, {}); }, }); @@ -77,7 +84,7 @@ function getPersonalDetailsByIDs(accountIDs: number[], currentUserAccountID: num } function getPersonalDetailByEmail(email: string): PersonalDetails | undefined { - return (Object.values(allPersonalDetails ?? {}) as PersonalDetails[]).find((detail) => detail?.login === email); + return emailToPersonalDetailsCache[email.toLowerCase()]; } /** From 9c11da7ca66010eb2817f4999b5a24eb72168794 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 3 Apr 2024 10:45:02 +0200 Subject: [PATCH 076/127] CR fixes --- src/components/SelectionList/BaseSelectionList.tsx | 5 ++++- src/pages/EditReportFieldDropdownPage.tsx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index a44e6d02c19b..c8f442e6ccbb 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -521,7 +521,10 @@ 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/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 575f77e88b64..75c2a9c5be26 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -145,6 +145,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, onChangeText={setSearchValue} isRowMultilineSupported headerMessage={headerMessage} + initiallyFocusedOptionKey={fieldValue} /> )} From cf08e809cc19379cfe390f6ca082543aad09476c Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 3 Apr 2024 09:49:48 +0100 Subject: [PATCH 077/127] chore(dateutilstest): revert selected timezone change to prevent tests failing because of daylight saving time --- tests/unit/DateUtilsTest.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/unit/DateUtilsTest.ts b/tests/unit/DateUtilsTest.ts index a7f43ea84045..9df0113168e4 100644 --- a/tests/unit/DateUtilsTest.ts +++ b/tests/unit/DateUtilsTest.ts @@ -15,8 +15,20 @@ describe('DateUtils', () => { Onyx.init({ keys: ONYXKEYS, initialKeyStates: { - [ONYXKEYS.SESSION]: {accountID: 999}, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: {'999': {accountID: 999, timezone: {selected: 'Europe/London'}}}, + [ONYXKEYS.SESSION]: { + accountID: 999, + }, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + '999': { + accountID: 999, + timezone: { + // UTC is not recognized as a valid timezone but + // in these tests we want to use it to avoid issues + // because of daylight saving time + selected: UTC as SelectedTimezone, + }, + }, + }, }, }); return waitForBatchedUpdates(); From c27d74eba12daac113f252775bae0e50059546af Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 3 Apr 2024 12:02:57 +0300 Subject: [PATCH 078/127] Updated comment --- src/components/Composer/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 57e56c9a57ea..035f54dde7ec 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -388,9 +388,9 @@ function Composer( if (isReportActionCompose) { ReportActionComposeFocusManager.onComposerFocus(null); } else { - // While a user was editing a comment and if they open on LHN menu we want the focus to return - // to the ReportActionItemMessageEdit compose after they click on the menu (for e.g. mark as read) - // so we assign the focus callback here. + // While a user edits a comment, if they open the LHN menu, we want to ensure that + // the focus returns to the message edit composer after they click on a menu item (e.g. mark as read). + // To achieve this, we re-assign the focus callback here. ReportActionComposeFocusManager.onComposerFocus(() => { if (!textInput.current) { return; From 4a40236d1888229a6d0cfbe45bc58087c0a966a3 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 3 Apr 2024 12:15:28 +0300 Subject: [PATCH 079/127] added comment --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index bd0c8ad76e4a..b9af592433b9 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -202,6 +202,8 @@ const ContextMenuActions: ContextMenuAction[] = [ if (closePopover) { hideContextMenu(false, () => { InteractionManager.runAfterInteractions(() => { + // Normally the focus callback of the main composer doesn't focus for natives, + // so we need to pass true here to allow focusing for natives too. ReportActionComposeFocusManager.focus(true); }); Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); From b6f8d0bbca2f1f5fbb71344117b4cfc348ffb039 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 10:26:26 +0500 Subject: [PATCH 080/127] 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 b33642ea363f499e8cf69a4f6f3f2b03c32f54da Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 4 Apr 2024 06:30:15 +0100 Subject: [PATCH 081/127] Use correct isGroupChat condition when creating a report --- src/libs/actions/Report.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f5315f2c8006..3765faedf537 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -211,12 +211,6 @@ Onyx.connect({ callback: (val) => (allRecentlyUsedReportFields = val), }); -let newGroupDraft: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT, - callback: (value) => (newGroupDraft = value), -}); - function clearGroupChat() { Onyx.set(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, null); } @@ -798,14 +792,15 @@ function navigateToAndOpenReport(userLogins: string[], shouldDismissModal = true let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; let chat: OnyxEntry | EmptyObject = {}; const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); + const isGroupChat = participantAccountIDs.length > 1; // If we are not creating a new Group Chat then we are creating a 1:1 DM and will look for an existing chat - if (!newGroupDraft) { + if (!isGroupChat) { chat = ReportUtils.getChatByParticipants(participantAccountIDs); } if (isEmptyObject(chat)) { - if (newGroupDraft) { + if (isGroupChat) { newChat = ReportUtils.buildOptimisticChatReport( participantAccountIDs, reportName, From 13c9220a3caf623263cae502fdfd8e162eb373a3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 4 Apr 2024 12:35:28 +0700 Subject: [PATCH 082/127] Fix three-dot menu item display incorrectly when opening receipt in combine report --- src/components/ReportActionItem/ReportActionItemImage.tsx | 6 ++++-- src/components/ShowContextMenuContext.ts | 2 ++ src/pages/home/report/ReportActionItem.tsx | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 740f784d2669..400d61b782b6 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -95,10 +95,12 @@ function ReportActionItemImage({ if (enablePreviewModal) { return ( - {({report}) => ( + {({report, transactionThreadReport}) => ( Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(report?.reportID ?? '', transaction?.transactionID ?? ''))} + onPress={() => + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(transactionThreadReport?.reportID ?? report?.reportID ?? '', transaction?.transactionID ?? '')) + } accessibilityLabel={translate('accessibilityHints.viewAttachment')} accessibilityRole={CONST.ROLE.BUTTON} > diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 374ca8a2f1e5..3a996a8d2c64 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -13,6 +13,7 @@ type ShowContextMenuContextProps = { anchor: ContextMenuAnchor; report: OnyxEntry; action: OnyxEntry; + transactionThreadReport: OnyxEntry; checkIfContextMenuActive: () => void; }; @@ -20,6 +21,7 @@ const ShowContextMenuContext = createContext({ anchor: null, report: null, action: null, + transactionThreadReport: null, checkIfContextMenuActive: () => {}, }); diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 485d75c9099b..980fc7217b1d 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -366,9 +366,10 @@ function ReportActionItem({ anchor: popoverAnchorRef.current, report, action, + transactionThreadReport, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, }), - [report, action, toggleContextMenuFromActiveReportAction], + [report, action, toggleContextMenuFromActiveReportAction, transactionThreadReport], ); const actionableItemButtons: ActionableItem[] = useMemo(() => { From 8ada1df16a1ec55476518c2c816096d6d46061de Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 4 Apr 2024 12:02:24 +0530 Subject: [PATCH 083/127] rename olddot hubs and update description --- docs/_data/_routes.yml | 93 ++++++++++--------- .../Pay-Bills.md | 0 .../Reimbursing-Reports.md | 0 .../Third-Party-Payments.md | 0 .../Global-Reimbursements.md | 0 .../Personal-Credit-Cards.md | 0 .../Business-Bank-Accounts-AUD.md | 0 .../Business-Bank-Accounts-USD.md | 0 .../company-cards/CSV-Import.md | 0 .../company-cards/Commercial-Card-Feeds.md | 0 .../company-cards/Company-Card-Settings.md | 0 .../company-cards/Connect-ANZ.md | 0 .../company-cards/Direct-Bank-Connections.md | 0 .../company-cards/Reconciliation.md | 0 .../company-cards/Troubleshooting.md | 0 .../deposit-accounts/Deposit-Accounts-AUD.md | 0 .../deposit-accounts/Deposit-Accounts-USD.md | 0 .../Custom-Templates.md | 0 .../Default-Export-Templates.md | 0 .../Fringe-Benefits.md | 0 .../Insights.md | 0 .../Other-Export-Options.md | 0 .../index.html | 2 +- .../business-bank-accounts.html | 0 .../company-cards.html | 0 .../deposit-accounts.html | 0 .../index.html | 2 +- .../index.html | 2 +- 28 files changed, 50 insertions(+), 49 deletions(-) rename docs/articles/expensify-classic/{send-payments => bank-accounts-and-payments}/Pay-Bills.md (100%) rename docs/articles/expensify-classic/{send-payments => bank-accounts-and-payments}/Reimbursing-Reports.md (100%) rename docs/articles/expensify-classic/{send-payments => bank-accounts-and-payments}/Third-Party-Payments.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/Global-Reimbursements.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/Personal-Credit-Cards.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/business-bank-accounts/Business-Bank-Accounts-AUD.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/business-bank-accounts/Business-Bank-Accounts-USD.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/CSV-Import.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/Commercial-Card-Feeds.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/Company-Card-Settings.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/Connect-ANZ.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/Direct-Bank-Connections.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/Reconciliation.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards/Troubleshooting.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/deposit-accounts/Deposit-Accounts-AUD.md (100%) rename docs/articles/expensify-classic/{bank-accounts-and-credit-cards => connect-credit-cards}/deposit-accounts/Deposit-Accounts-USD.md (100%) rename docs/articles/expensify-classic/{insights-and-custom-reporting => spending-insights}/Custom-Templates.md (100%) rename docs/articles/expensify-classic/{insights-and-custom-reporting => spending-insights}/Default-Export-Templates.md (100%) rename docs/articles/expensify-classic/{insights-and-custom-reporting => spending-insights}/Fringe-Benefits.md (100%) rename docs/articles/expensify-classic/{insights-and-custom-reporting => spending-insights}/Insights.md (100%) rename docs/articles/expensify-classic/{insights-and-custom-reporting => spending-insights}/Other-Export-Options.md (100%) rename docs/expensify-classic/hubs/{bank-accounts-and-credit-cards => bank-accounts-and-payments}/index.html (56%) rename docs/expensify-classic/hubs/{bank-accounts-and-credit-cards => connect-credit-cards}/business-bank-accounts.html (100%) rename docs/expensify-classic/hubs/{bank-accounts-and-credit-cards => connect-credit-cards}/company-cards.html (100%) rename docs/expensify-classic/hubs/{bank-accounts-and-credit-cards => connect-credit-cards}/deposit-accounts.html (100%) rename docs/expensify-classic/hubs/{send-payments => connect-credit-cards}/index.html (62%) rename docs/expensify-classic/hubs/{insights-and-custom-reporting => spending-insights}/index.html (65%) diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index 3d0d16b00587..c04ed86bca9e 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -17,73 +17,74 @@ platforms: - href: getting-started title: Getting Started icon: /assets/images/accounting.svg - description: From setting up your account to ensuring you get the most out of Expensify’s suite of features, click here to get started on streamlining your expense management journey. + description: Set up your account to optimize Expensify's features. - - href: settings - title: Settings - icon: /assets/images/gears.svg - description: Discover how to personalize your profile, add secondary logins, and grant delegated access to employees with our comprehensive guide on Account Settings. - - - href: bank-accounts-and-credit-cards - title: Bank Accounts & Credit Cards - icon: /assets/images/bank-card.svg - description: Find out how to connect Expensify to your financial institutions, track credit card transactions, and best practices for reconciling company cards. + - href: workspaces + title: Workspaces + icon: /assets/images/shield.svg + description: Configure rules, settings, and limits for your company’s spending. - - href: expensify-billing - title: Expensify Billing - icon: /assets/images/subscription-annual.svg - description: Review Expensify's subscription options, plan types, and payment methods. + - href: expenses + title: Expenses + icon: /assets/images/money-into-wallet.svg + description: Learn more about expense tracking and submission. - href: reports title: Reports icon: /assets/images/money-receipt.svg description: Set approval workflows and use Expensify’s automated report features. + - href: domains + title: Domains + icon: /assets/images/domains.svg + description: Claim and verify your company’s domain to access additional management and security features. + + - href: bank-accounts-and-payments + title: Bank Accounts & Payments + icon: /assets/images/send-money.svg + description: Send direct reimbursements, pay invoices, and receive payment. + + - href: connect-credit-cards + title: Connect Credit Cards + icon: /assets/images/bank-card.svg + description: Track credit card transactions and reconcile company cards. + - href: expensify-card title: Expensify Card icon: /assets/images/hand-card.svg - description: Explore how the Expensify Card combines convenience and security to enhance everyday business transactions. Discover how to apply for, oversee, and maximize your card perks here. + description: Explore the perks and benefits of the Expensify Card. + + - href: copilots-and-delegates + title: Copilots & Delegates + icon: /assets/images/envelope-receipt.svg + description: Assign Copilots and delegate report approvals. - href: expensify-partner-program title: Expensify Partner Program icon: /assets/images/handshake.svg - description: Discover how to get the most out of Expensify as an ExpensifyApproved! accountant partner. Learn how to set up your clients, receive CPE credits, and take advantage of your partner discount. - - - href: expenses - title: Expenses - icon: /assets/images/money-into-wallet.svg - description: Learn more about expense tracking and submission. - - - href: insights-and-custom-reporting - title: Insights & Custom Reporting - icon: /assets/images/monitor.svg - description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. + description: Discover the benefits of becoming an Expensify Partner. - href: integrations title: Integrations icon: /assets/images/workflow.svg - description: Enhance Expensify’s capabilities by integrating it with your accounting or HR software. Here is where you can learn more about creating a synchronized financial management ecosystem. + description: Integrate with accounting or HR software to streamline expense approvals. - - href: copilots-and-delegates - title: Copilots & Delegates - icon: /assets/images/envelope-receipt.svg - description: Assign Copilots and delegate report approvals. - - - href: send-payments - title: Send Payments - icon: /assets/images/send-money.svg - description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. - - - href: workspaces - title: Workspaces - icon: /assets/images/shield.svg - description: Configure rules, settings, and limits for your company’s spending. + - href: spending-insights + title: Spending Insights + icon: /assets/images/monitor.svg + description: Create custom export templates to understand spending insights. - - href: domains - title: Domains - icon: /assets/images/domains.svg - description: Claim and verify your company’s domain to access additional management and security features. - + - href: settings + title: Settings + icon: /assets/images/gears.svg + description: Manage profile settings and notifications. + + - href: expensify-billing + title: Expensify Billing + icon: /assets/images/subscription-annual.svg + description: Review Expensify's subscription options, plan types, and payment methods. + + - href: new-expensify title: New Expensify hub-title: New Expensify - Help & Resources diff --git a/docs/articles/expensify-classic/send-payments/Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md similarity index 100% rename from docs/articles/expensify-classic/send-payments/Pay-Bills.md rename to docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md diff --git a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md similarity index 100% rename from docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md rename to docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md diff --git a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments.md similarity index 100% rename from docs/articles/expensify-classic/send-payments/Third-Party-Payments.md rename to docs/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements.md b/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements.md rename to docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md b/docs/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md rename to docs/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md b/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md rename to docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md rename to docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Connect-ANZ.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/Connect-ANZ.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md rename to docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md b/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md rename to docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md similarity index 100% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md rename to docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates.md b/docs/articles/expensify-classic/spending-insights/Custom-Templates.md similarity index 100% rename from docs/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates.md rename to docs/articles/expensify-classic/spending-insights/Custom-Templates.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md b/docs/articles/expensify-classic/spending-insights/Default-Export-Templates.md similarity index 100% rename from docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md rename to docs/articles/expensify-classic/spending-insights/Default-Export-Templates.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md b/docs/articles/expensify-classic/spending-insights/Fringe-Benefits.md similarity index 100% rename from docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md rename to docs/articles/expensify-classic/spending-insights/Fringe-Benefits.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md b/docs/articles/expensify-classic/spending-insights/Insights.md similarity index 100% rename from docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md rename to docs/articles/expensify-classic/spending-insights/Insights.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md b/docs/articles/expensify-classic/spending-insights/Other-Export-Options.md similarity index 100% rename from docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md rename to docs/articles/expensify-classic/spending-insights/Other-Export-Options.md diff --git a/docs/expensify-classic/hubs/bank-accounts-and-credit-cards/index.html b/docs/expensify-classic/hubs/bank-accounts-and-payments/index.html similarity index 56% rename from docs/expensify-classic/hubs/bank-accounts-and-credit-cards/index.html rename to docs/expensify-classic/hubs/bank-accounts-and-payments/index.html index 2f91f0913d6e..22e39250aea4 100644 --- a/docs/expensify-classic/hubs/bank-accounts-and-credit-cards/index.html +++ b/docs/expensify-classic/hubs/bank-accounts-and-payments/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Bank Accounts & Credit Cards +title: Bank accounts & payments --- {% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/bank-accounts-and-credit-cards/business-bank-accounts.html b/docs/expensify-classic/hubs/connect-credit-cards/business-bank-accounts.html similarity index 100% rename from docs/expensify-classic/hubs/bank-accounts-and-credit-cards/business-bank-accounts.html rename to docs/expensify-classic/hubs/connect-credit-cards/business-bank-accounts.html diff --git a/docs/expensify-classic/hubs/bank-accounts-and-credit-cards/company-cards.html b/docs/expensify-classic/hubs/connect-credit-cards/company-cards.html similarity index 100% rename from docs/expensify-classic/hubs/bank-accounts-and-credit-cards/company-cards.html rename to docs/expensify-classic/hubs/connect-credit-cards/company-cards.html diff --git a/docs/expensify-classic/hubs/bank-accounts-and-credit-cards/deposit-accounts.html b/docs/expensify-classic/hubs/connect-credit-cards/deposit-accounts.html similarity index 100% rename from docs/expensify-classic/hubs/bank-accounts-and-credit-cards/deposit-accounts.html rename to docs/expensify-classic/hubs/connect-credit-cards/deposit-accounts.html diff --git a/docs/expensify-classic/hubs/send-payments/index.html b/docs/expensify-classic/hubs/connect-credit-cards/index.html similarity index 62% rename from docs/expensify-classic/hubs/send-payments/index.html rename to docs/expensify-classic/hubs/connect-credit-cards/index.html index c8275af5c353..e21df799a132 100644 --- a/docs/expensify-classic/hubs/send-payments/index.html +++ b/docs/expensify-classic/hubs/connect-credit-cards/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Send Payments +title: Connect Credit Cards --- {% include hub.html %} \ No newline at end of file diff --git a/docs/expensify-classic/hubs/insights-and-custom-reporting/index.html b/docs/expensify-classic/hubs/spending-insights/index.html similarity index 65% rename from docs/expensify-classic/hubs/insights-and-custom-reporting/index.html rename to docs/expensify-classic/hubs/spending-insights/index.html index 16c96cb51d01..68b302394ff3 100644 --- a/docs/expensify-classic/hubs/insights-and-custom-reporting/index.html +++ b/docs/expensify-classic/hubs/spending-insights/index.html @@ -1,6 +1,6 @@ --- layout: default -title: Exports +title: Spending Insights --- {% include hub.html %} \ No newline at end of file From 8b771adae3e3d12083a4f411a98274708da3d9e9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 4 Apr 2024 07:35:37 +0100 Subject: [PATCH 084/127] Remove unused imported type --- src/libs/actions/Report.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3765faedf537..e885024c0618 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -69,16 +69,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; -import type { - NewGroupChatDraft, - PersonalDetails, - PersonalDetailsList, - PolicyReportField, - RecentlyUsedReportFields, - ReportActionReactions, - ReportMetadata, - ReportUserIsTyping, -} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; From 62528f3b87d02c17f480e0d6957900ae98f593e4 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 4 Apr 2024 09:11:27 +0200 Subject: [PATCH 085/127] Add textInputAutoFocus prop --- src/components/SelectionList/BaseSelectionList.tsx | 6 +++++- src/components/SelectionList/types.ts | 3 +++ src/pages/WorkspaceSwitcherPage.tsx | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index fdcee89899a8..dc69dfd39989 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -73,6 +73,7 @@ function BaseSelectionList( sectionTitleStyles, headerMessageStyle, shouldHideListOnInitialRender = true, + textInputAutoFocus = true, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -379,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) { @@ -393,7 +397,7 @@ function BaseSelectionList( } clearTimeout(focusTimeoutRef.current); }; - }, [shouldShowTextInput]), + }, [shouldShowTextInput, textInputAutoFocus]), ); const prevTextInputValue = usePrevious(textInputValue); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 549cd9de87df..f4b1b990811f 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -221,6 +221,9 @@ type BaseSelectionListProps = Partial & { /** 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; diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 6ce5457aab99..406447d04c05 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -245,6 +245,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { containerStyle={[styles.pt0, styles.mt0]} textInputIconLeft={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? MagnifyingGlass : undefined} initiallyFocusedOptionKey={activeWorkspaceID} + textInputAutoFocus={false} /> ) : ( From df605a8bde785320a606b4ef269d2415b3d9c4aa Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Thu, 4 Apr 2024 09:12:27 +0200 Subject: [PATCH 086/127] Fix/App does not redirect to group chat when splitting bill via Shortcut --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5c92cd87a2bc..8a137fb30ba0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2771,7 +2771,7 @@ function splitBill( API.write(WRITE_COMMANDS.SPLIT_BILL, parameters, onyxData); resetMoneyRequestInfo(); - Navigation.dismissModal(); + Navigation.dismissModal(existingSplitChatReportID); Report.notifyNewAction(splitData.chatReportID, currentUserAccountID); } From f044d837037bfd4a4fd6c8fa5947868023f57f16 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 4 Apr 2024 09:32:21 +0200 Subject: [PATCH 087/127] Remove autoFocus on WorkspaceSwitcher --- src/pages/WorkspaceSwitcherPage.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 406447d04c05..5905e39310da 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -14,7 +14,6 @@ import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; @@ -59,7 +58,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const [searchTerm, setSearchTerm] = useState(''); - const {inputCallbackRef} = useAutoFocusInput(); const {translate} = useLocalize(); const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); @@ -235,7 +233,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { ), - [ - inputCallbackRef, - setSearchTerm, - searchTerm, - selectPolicy, - styles, - theme.textSupporting, - translate, - usersWorkspaces.length, - usersWorkspacesSectionData, - activeWorkspaceID, - theme.icon, - headerMessage, - ], + [setSearchTerm, searchTerm, selectPolicy, styles, theme.textSupporting, translate, usersWorkspaces.length, usersWorkspacesSectionData, activeWorkspaceID, theme.icon, headerMessage], ); return ( From 3c3e295060ff666e012ed128d2183b98c14eeff8 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 4 Apr 2024 09:53:21 +0200 Subject: [PATCH 088/127] revert unnecessary change --- src/pages/WorkspaceSwitcherPage.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 5905e39310da..406447d04c05 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -14,6 +14,7 @@ import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; @@ -58,6 +59,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const [searchTerm, setSearchTerm] = useState(''); + const {inputCallbackRef} = useAutoFocusInput(); const {translate} = useLocalize(); const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); @@ -233,6 +235,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { ), - [setSearchTerm, searchTerm, selectPolicy, styles, theme.textSupporting, translate, usersWorkspaces.length, usersWorkspacesSectionData, activeWorkspaceID, theme.icon, headerMessage], + [ + inputCallbackRef, + setSearchTerm, + searchTerm, + selectPolicy, + styles, + theme.textSupporting, + translate, + usersWorkspaces.length, + usersWorkspacesSectionData, + activeWorkspaceID, + theme.icon, + headerMessage, + ], ); return ( From 253f3dbbc760baf443b49492ee231b6920f029cf Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 15:45:45 +0500 Subject: [PATCH 089/127] 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 090/127] 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 091/127] 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 092/127] 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 093/127] 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 094/127] 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 095/127] 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 6e2e2c991f90d892b44e7a5653c446cfe1414863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 4 Apr 2024 16:48:45 +0200 Subject: [PATCH 096/127] merge fixes and results not found fix --- .../ShareLogList/BaseShareLogList.tsx | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index cd77147e3aef..578efbe5317b 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -1,9 +1,7 @@ import React, {useMemo} from 'react'; -import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useBetas, usePersonalDetails} from '@components/OnyxProvider'; +import {useBetas} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; -import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -13,20 +11,16 @@ import useNetwork from '@hooks/useNetwork'; 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 CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; -import type {BaseShareLogListOnyxProps, BaseShareLogListProps} from './types'; +import type {BaseShareLogListProps} from './types'; -function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) { +function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const personalDetails = usePersonalDetails(); const betas = useBetas(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); const {options, areOptionsInitialized} = useOptionsList(); const searchOptions = useMemo(() => { @@ -44,11 +38,7 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) userToInvite: localUserToInvite, } = OptionsListUtils.getShareLogOptions(options, debouncedSearchValue.trim(), betas ?? []); - const header = OptionsListUtils.getHeaderMessage( - (searchOptions?.recentReports?.length || 0) + (searchOptions?.personalDetails?.length || 0) !== 0, - Boolean(searchOptions?.userToInvite), - debouncedSearchValue, - ); + const header = OptionsListUtils.getHeaderMessage((localRecentReports?.length || 0) + (localPersonalDetails?.length || 0) !== 0, Boolean(localUserToInvite), debouncedSearchValue); return { recentReports: localRecentReports, @@ -56,7 +46,7 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) userToInvite: localUserToInvite, headerMessage: header, }; - }, [isOptionsDataReady, reports, personalDetails, debouncedSearchValue, betas]); + }, [areOptionsInitialized, options, debouncedSearchValue, betas]); const sections = useMemo(() => { const sectionsList = []; @@ -112,7 +102,6 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) headerMessage={searchOptions.headerMessage} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputHint={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} - autoFocus showLoadingPlaceholder={!didScreenTransitionEnd} /> @@ -123,8 +112,4 @@ function BaseShareLogList({reports, onAttachLogToReport}: BaseShareLogListProps) BaseShareLogList.displayName = 'ShareLogPage'; -export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, -})(BaseShareLogList); +export default BaseShareLogList; From 5194168e0c1ff0abe553ac5b18924e6a5631c16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 4 Apr 2024 16:55:52 +0200 Subject: [PATCH 097/127] type fixes --- src/pages/settings/AboutPage/ShareLogList/types.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/pages/settings/AboutPage/ShareLogList/types.ts b/src/pages/settings/AboutPage/ShareLogList/types.ts index 6d385e6cac74..500641a3da42 100644 --- a/src/pages/settings/AboutPage/ShareLogList/types.ts +++ b/src/pages/settings/AboutPage/ShareLogList/types.ts @@ -1,18 +1,10 @@ -import type {OnyxCollection} from 'react-native-onyx'; -import type {Report} from '@src/types/onyx'; - -type BaseShareLogListOnyxProps = { - /** All reports shared with the user */ - reports: OnyxCollection; -}; - type ShareLogListProps = { /** The source of the log file to share */ logSource: string; }; -type BaseShareLogListProps = BaseShareLogListOnyxProps & { +type BaseShareLogListProps = { onAttachLogToReport: (reportID: string, filename: string) => void; }; -export type {BaseShareLogListOnyxProps, BaseShareLogListProps, ShareLogListProps}; +export type {BaseShareLogListProps, ShareLogListProps}; From 0aa35266cf99a16778aaf6786125e6ce7b5f0e53 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Apr 2024 20:40:44 +0100 Subject: [PATCH 098/127] make transaction in getDefaultTaxName optional --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 430100e84b2f..af28133ecf3f 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -623,7 +623,7 @@ function getEnabledTaxRateCount(options: TaxRates) { /** * Gets the default tax name */ -function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction: Transaction) { +function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction?: Transaction) { const defaultTaxKey = taxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${Localize.translateLocal('common.default')}`) || ''; return transaction?.taxRate?.text ?? defaultTaxName; From be564603f77f30bf5812f92a8a965659ffce6418 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Apr 2024 20:40:59 +0100 Subject: [PATCH 099/127] fix taxPicker selection for WorkspaceTaxesSettingsForeignCurrency --- .../WorkspaceTaxesSettingsForeignCurrency.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx index dbe94ba802ef..5fd92310874c 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx @@ -3,13 +3,13 @@ import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import type {ListItem} from '@components/SelectionList/types'; import TaxPicker from '@components/TaxPicker'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {setForeignCurrencyDefault} from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import type * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; @@ -22,7 +22,6 @@ import type SCREENS from '@src/SCREENS'; type WorkspaceTaxesSettingsForeignCurrencyProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; - function WorkspaceTaxesSettingsForeignCurrency({ route: { params: {policyID}, @@ -32,10 +31,15 @@ function WorkspaceTaxesSettingsForeignCurrency({ const {translate} = useLocalize(); const styles = useThemeStyles(); - const selectedTaxRate = TransactionUtils.getTaxName(policy?.taxRates?.taxes ?? {}, policy?.taxRates?.foreignTaxDefault ?? ''); + const taxRates = policy?.taxRates; + const taxes = taxRates?.taxes ?? {}; + const defaultTaxKey = taxRates?.foreignTaxDefault ?? ''; + const defaultExternalID = taxRates?.defaultExternalID ?? ''; + + const selectedTaxRate = defaultTaxKey === defaultExternalID ? taxRates && TransactionUtils.getDefaultTaxName(taxRates) : TransactionUtils.getTaxName(taxes, defaultTaxKey); - const submit = ({keyForList}: ListItem) => { - setForeignCurrencyDefault(policyID, keyForList ?? ''); + const submit = (taxes: OptionsListUtils.TaxRatesOption) => { + setForeignCurrencyDefault(policyID, taxes.data.code ?? ''); Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID)); }; @@ -59,6 +63,7 @@ function WorkspaceTaxesSettingsForeignCurrency({ From bd3788fa2fe55586fa53dbb71cac7f54394cfdf3 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Apr 2024 20:41:08 +0100 Subject: [PATCH 100/127] fix taxPicker selection for WorkspaceTaxesSettingsWorkspaceCurrency --- .../taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx index c6de23069837..630560f864b4 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx @@ -3,13 +3,13 @@ import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import type {ListItem} from '@components/SelectionList/types'; import TaxPicker from '@components/TaxPicker'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceCurrencyDefault} from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import type * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; @@ -32,9 +32,10 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ const {translate} = useLocalize(); const styles = useThemeStyles(); - const selectedTaxRate = TransactionUtils.getTaxName(policy?.taxRates?.taxes ?? {}, policy?.taxRates?.foreignTaxDefault ?? ''); - const submit = ({keyForList}: ListItem) => { - setWorkspaceCurrencyDefault(policyID, keyForList ?? ''); + const selectedTaxRate = policy?.taxRates && TransactionUtils.getDefaultTaxName(policy?.taxRates); + + const submit = (taxes: OptionsListUtils.TaxRatesOption) => { + setWorkspaceCurrencyDefault(policyID, taxes.data.code ?? ''); Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID)); }; @@ -58,6 +59,7 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ From fbf19fbc09ea06698d682dfc8fbfa8392f508c5f Mon Sep 17 00:00:00 2001 From: aimeebarigian <54041188+aimeebarigian@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:56:15 -0500 Subject: [PATCH 101/127] Update redirects.csv Redirecting dead helpdot links with the correct redirects --- docs/redirects.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 32fc61642bda..d58919512abd 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -77,3 +77,5 @@ https://help.expensify.com/articles/expensify-classic/workspace-and-domain-setti https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Reimbursement,https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing-Reports https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Tags https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles,https://help.expensify.com/articles/expensify-classic/workspaces/Change-member-workspace-roles +https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/tax-tracking,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Apply-Tax +https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles.html,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/ From 4fc604ddddc15b87421f32d13ba9a7686ea0bfbd Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Apr 2024 21:13:09 +0100 Subject: [PATCH 102/127] fix review comment --- .../taxes/WorkspaceTaxesSettingsForeignCurrency.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx index 5fd92310874c..3081df55fe69 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx @@ -32,11 +32,11 @@ function WorkspaceTaxesSettingsForeignCurrency({ const styles = useThemeStyles(); const taxRates = policy?.taxRates; - const taxes = taxRates?.taxes ?? {}; - const defaultTaxKey = taxRates?.foreignTaxDefault ?? ''; + const foreignTaxDefault = taxRates?.foreignTaxDefault ?? ''; const defaultExternalID = taxRates?.defaultExternalID ?? ''; - const selectedTaxRate = defaultTaxKey === defaultExternalID ? taxRates && TransactionUtils.getDefaultTaxName(taxRates) : TransactionUtils.getTaxName(taxes, defaultTaxKey); + const selectedTaxRate = + foreignTaxDefault === defaultExternalID ? taxRates && TransactionUtils.getDefaultTaxName(taxRates) : TransactionUtils.getTaxName(taxRates?.taxes ?? {}, foreignTaxDefault); const submit = (taxes: OptionsListUtils.TaxRatesOption) => { setForeignCurrencyDefault(policyID, taxes.data.code ?? ''); From 936b9dc9ac705d8e0ca31cf82e50e05f60f3200d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 5 Apr 2024 02:29:25 +0500 Subject: [PATCH 103/127] 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 104/127] 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 ceec51ed9e582fe9d4cdf93fadbb6797b9cce6e4 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 4 Apr 2024 17:29:22 -0600 Subject: [PATCH 105/127] Revert "Defer local updates if there are missing updates and only call `GetMissingOnyxMessages` once" --- src/libs/actions/OnyxUpdateManager.ts | 162 ++------------------------ 1 file changed, 10 insertions(+), 152 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 45cb2b78ecde..9c6f30cc5e9e 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -3,7 +3,6 @@ import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; import * as App from './App'; import * as OnyxUpdates from './OnyxUpdates'; @@ -28,134 +27,6 @@ Onyx.connect({ callback: (value) => (lastUpdateIDAppliedToClient = value), }); -let queryPromise: Promise | undefined; - -type DeferredUpdatesDictionary = Record; -let deferredUpdates: DeferredUpdatesDictionary = {}; - -// This function will reset the query variables, unpause the SequentialQueue and log an info to the user. -function finalizeUpdatesAndResumeQueue() { - console.debug('[OnyxUpdateManager] Done applying all updates'); - queryPromise = undefined; - deferredUpdates = {}; - Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); - SequentialQueue.unpause(); -} - -// This function applies a list of updates to Onyx in order and resolves when all updates have been applied -const applyUpdates = (updates: DeferredUpdatesDictionary) => Promise.all(Object.values(updates).map((update) => OnyxUpdates.apply(update))); - -// In order for the deferred updates to be applied correctly in order, -// we need to check if there are any gaps between deferred updates. -type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; -function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSplitResult { - const updateValues = Object.values(updates); - const applicableUpdates: DeferredUpdatesDictionary = {}; - - let gapExists = false; - let firstUpdateAfterGaps: number | undefined; - let latestMissingUpdateID: number | undefined; - - for (const [index, update] of updateValues.entries()) { - const isFirst = index === 0; - - // If any update's previousUpdateID doesn't match the lastUpdateID from the previous update, the deferred updates aren't chained and there's a gap. - // For the first update, we need to check that the previousUpdateID of the fetched update is the same as the lastUpdateIDAppliedToClient. - // For any other updates, we need to check if the previousUpdateID of the current update is found in the deferred updates. - // If an update is chained, we can add it to the applicable updates. - const isChained = isFirst ? update.previousUpdateID === lastUpdateIDAppliedToClient : !!updates[Number(update.previousUpdateID)]; - if (isChained) { - // If a gap exists already, we will not add any more updates to the applicable updates. - // Instead, once there are two chained updates again, we can set "firstUpdateAfterGaps" to the first update after the current gap. - if (gapExists) { - // If "firstUpdateAfterGaps" hasn't been set yet and there was a gap, we need to set it to the first update after all gaps. - if (!firstUpdateAfterGaps) { - firstUpdateAfterGaps = Number(update.previousUpdateID); - } - } else { - // If no gap exists yet, we can add the update to the applicable updates - applicableUpdates[Number(update.lastUpdateID)] = update; - } - } else { - // When we find a (new) gap, we need to set "gapExists" to true and reset the "firstUpdateAfterGaps" variable, - // so that we can continue searching for the next update after all gaps - gapExists = true; - firstUpdateAfterGaps = undefined; - - // If there is a gap, it means the previous update is the latest missing update. - latestMissingUpdateID = Number(update.previousUpdateID); - } - } - - // When "firstUpdateAfterGaps" is not set yet, we need to set it to the last update in the list, - // because we will fetch all missing updates up to the previous one and can then always apply the last update in the deferred updates. - if (!firstUpdateAfterGaps) { - firstUpdateAfterGaps = Number(updateValues[updateValues.length - 1].lastUpdateID); - } - - let updatesAfterGaps: DeferredUpdatesDictionary = {}; - if (gapExists && firstUpdateAfterGaps) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - updatesAfterGaps = Object.fromEntries(Object.entries(updates).filter(([lastUpdateID]) => Number(lastUpdateID) >= firstUpdateAfterGaps!)); - } - - return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID}; -} - -// This function will check for gaps in the deferred updates and -// apply the updates in order after the missing updates are fetched and applied -function validateAndApplyDeferredUpdates(): Promise { - // We only want to apply deferred updates that are newer than the last update that was applied to the client. - // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. - const pendingDeferredUpdates = Object.fromEntries( - Object.entries(deferredUpdates).filter(([lastUpdateID]) => { - // It should not be possible for lastUpdateIDAppliedToClient to be null, - // after the missing updates have been applied. - // If still so we want to keep the deferred update in the list. - if (!lastUpdateIDAppliedToClient) { - return true; - } - return (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient; - }), - ); - - // If there are no remaining deferred updates after filtering out outdated ones, - // we can just unpause the queue and return - if (Object.values(pendingDeferredUpdates).length === 0) { - return Promise.resolve(); - } - - const {applicableUpdates, updatesAfterGaps, latestMissingUpdateID} = detectGapsAndSplit(pendingDeferredUpdates); - - // If we detected a gap in the deferred updates, only apply the deferred updates before the gap, - // re-fetch the missing updates and then apply the remaining deferred updates after the gap - if (latestMissingUpdateID) { - return new Promise((resolve, reject) => { - deferredUpdates = {}; - applyUpdates(applicableUpdates).then(() => { - // After we have applied the applicable updates, there might have been new deferred updates added. - // In the next (recursive) call of "validateAndApplyDeferredUpdates", - // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, - // as long as there was no new gap detected. Otherwise repeat the process. - deferredUpdates = {...deferredUpdates, ...updatesAfterGaps}; - - // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. - // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. - if (!lastUpdateIDAppliedToClient || latestMissingUpdateID <= lastUpdateIDAppliedToClient) { - validateAndApplyDeferredUpdates().then(resolve).catch(reject); - return; - } - - // Then we can fetch the missing updates and apply them - App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, latestMissingUpdateID).then(validateAndApplyDeferredUpdates).then(resolve).catch(reject); - }); - }); - } - - // If there are no gaps in the deferred updates, we can apply all deferred updates in order - return applyUpdates(applicableUpdates); -} - export default () => { console.debug('[OnyxUpdateManager] Listening for updates from the server'); Onyx.connect({ @@ -195,45 +66,32 @@ export default () => { // applied in their correct and specific order. If this queue was not paused, then there would be a lot of // onyx data being applied while we are fetching the missing updates and that would put them all out of order. SequentialQueue.pause(); + let canUnpauseQueuePromise; // The flow below is setting the promise to a reconnect app to address flow (1) explained above. if (!lastUpdateIDAppliedToClient) { - // If there is a ReconnectApp query in progress, we should not start another one. - if (queryPromise) { - return; - } - Log.info('Client has not gotten reliable updates before so reconnecting the app to start the process'); // Since this is a full reconnectApp, we'll not apply the updates we received - those will come in the reconnect app request. - queryPromise = App.finalReconnectAppAfterActivatingReliableUpdates(); + canUnpauseQueuePromise = App.finalReconnectAppAfterActivatingReliableUpdates(); } else { // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. - - // Get the number of deferred updates before adding the new one - const existingDeferredUpdatesCount = Object.keys(deferredUpdates).length; - - // Add the new update to the deferred updates - deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams; - - // If there are deferred updates already, we don't need to fetch the missing updates again. - if (existingDeferredUpdatesCount > 0) { - return; - } - console.debug(`[OnyxUpdateManager] Client is behind the server by ${Number(previousUpdateIDFromServer) - lastUpdateIDAppliedToClient} so fetching incremental updates`); Log.info('Gap detected in update IDs from server so fetching incremental updates', true, { lastUpdateIDFromServer, previousUpdateIDFromServer, lastUpdateIDAppliedToClient, }); - - // Get the missing Onyx updates from the server and afterwards validate and apply the deferred updates. - // This will trigger recursive calls to "validateAndApplyDeferredUpdates" if there are gaps in the deferred updates. - queryPromise = App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, previousUpdateIDFromServer).then(validateAndApplyDeferredUpdates); + canUnpauseQueuePromise = App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, previousUpdateIDFromServer); } - queryPromise.finally(finalizeUpdatesAndResumeQueue); + canUnpauseQueuePromise.finally(() => { + OnyxUpdates.apply(updateParams).finally(() => { + console.debug('[OnyxUpdateManager] Done applying all updates'); + Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); + SequentialQueue.unpause(); + }); + }); }, }); }; From 17b434ab406ae816439f087080a6effdd1db6c1a Mon Sep 17 00:00:00 2001 From: Lauren Schurr <33293730+lschurr@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:34:24 -0700 Subject: [PATCH 106/127] Delete docs/articles/expensify-classic/reports/The-Expenses-Page.md --- .../reports/The-Expenses-Page.md | 74 ------------------- 1 file changed, 74 deletions(-) delete mode 100644 docs/articles/expensify-classic/reports/The-Expenses-Page.md diff --git a/docs/articles/expensify-classic/reports/The-Expenses-Page.md b/docs/articles/expensify-classic/reports/The-Expenses-Page.md deleted file mode 100644 index 57a7f7de298c..000000000000 --- a/docs/articles/expensify-classic/reports/The-Expenses-Page.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: The Expenses Page -description: Details on Expenses Page filters ---- -# Overview - -The Expenses page allows you to see all of your personal expenses. If you are an admin, you can view all submitter’s expenses on the Expensify page. The Expenses page can be filtered in several ways to give you spending visibility, find expenses to submit and export to a spreadsheet (CSV). - -## Expense filters -Here are the available filters you can use on the Expenses Page: - -- **Date Range:** Find expenses within a specific time frame. -- **Merchant Name:** Search for expenses from a particular merchant. (Partial search terms also work if you need clarification on the exact name match.) -- **Workspace:** Locate specific Group/Individual Workspace expenses. -- **Categories:** Group expenses by category or identify those without a category. -- **Tags:** Filter expenses with specific tags. -- **Submitters:** Narrow expenses by submitter (employee or vendor). -- **Personal Expenses:** Find all expenses yet to be included in a report. A Workspace admin can see these expenses once they are on a Processing, Approved, or Reimbursed report. -- **Open:** Display expenses on reports that still need to be submitted (not submitted). -- **Processing, Approved, Reimbursed:** See expenses on reports at various stages – processing, approved, or reimbursed. -- **Closed:** View expenses on closed reports (not submitted for approval). - -Here's how to make the most of these filters: - -1. Log into your web account -2. Go to the **Expenses** page -3. At the top of the page, click on **Show Filters** -4. Adjust the filters to match your specific needs - -Note, you might notice that not all expense filters are always visible. They adapt based on the data you're currently filtering and persist from the last time you logged in. For instance, you won't see the deleted filter if there are no **Deleted** expenses to filter out. - -If you are not seeing what you expected, you may have too many filters applied. Click **Reset** at the top to clear your filters. - - -# How to add an expense to a report from the Expenses Page -The submitter (and their copilot) can add expenses to a report from the Expenses page. - -Note, when expenses aren’t on a report, they are **personal expenses**. So you’ll want to make sure you haven’t filtered out **personal expenses** expenses, or you won’t be able to see them. - -1. Find the expense you want to add. (Hint: Use the filters to sort expenses by the desired date range if it is not a recent expense.) -2. Then, select the expense you want to add to a report. You can click Select All to select multiple expenses. -3. Click **Add to Report** in the upper right corner, and choose either an existing report or create a new one. - -# How to code expenses from the Expenses Page -To code expenses from the Expenses page, do the following: - -1. Look for the **Tag**, **Category**, and **Description** columns on the **Expenses** page. -2. Click on the relevant field for a specific expense and add or update the **Category**, **Tag**, or **Description**. - -Note, you can also open up individual expenses by clicking on them to see a detailed look, but coding the expenses from the Expense list is even faster and more convenient! - -# How to export expenses to a CSV file or spreadsheet -If you want to export multiple expenses, run through the below steps: -Select the expenses you want to export by checking the box to the left of each expense. -Then, click **Export To** in the upper right corner of the page, and choose our default CSV format or create your own custom CSV template. - - -{% include faq-begin.md %} - -## Can I use the filters and analytics features on the mobile app? -The various features on the Expenses Page are only available while logged into your web account. - -## As a Workspace admin, what submitter expenses can you see? -A Workspace admin can see Processing, Approved, and Reimbursed expenses as long as they were submitted on the workspace that you are an admin. - -If employees submit expense reports on a workspace where you are not an admin, you will not have visibility into those expenses. Additionally, if an expense is left unreported, a workspace admin will not be able to see that expense until it’s been added to a report. - -A Workspace admin can edit the tags and categories on an expense, but if they want to edit the amount, date, or merchant name, the expense will need to be in a Processing state or rejected back to the submitter for changes. -We have more about company card expense reconciliation in this [support article](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation). - -## Can I edit multiple expenses at once? -Yes! Select the expenses you want to edit and click **Edit Multiple**. - -{% include faq-end.md %} From 16d9c5ca95bb9865f1313a01ac706410b84b753c Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Fri, 5 Apr 2024 02:02:35 +0100 Subject: [PATCH 107/127] fix autofocus on edit tax amount screen --- src/pages/EditRequestTaxAmountPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/EditRequestTaxAmountPage.tsx b/src/pages/EditRequestTaxAmountPage.tsx index a34ed8a5252d..2b6a6169637a 100644 --- a/src/pages/EditRequestTaxAmountPage.tsx +++ b/src/pages/EditRequestTaxAmountPage.tsx @@ -24,13 +24,13 @@ type EditRequestTaxAmountPageProps = { function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onSubmit}: EditRequestTaxAmountPageProps) { const {translate} = useLocalize(); - const textInput = useRef(null); + const textInput = useRef(); const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { - focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => textInput.current?.focus(), CONST.ANIMATED_TRANSITION); return () => { if (!focusTimeoutRef.current) { return; @@ -52,7 +52,7 @@ function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurre currency={defaultCurrency} amount={defaultAmount} taxAmount={defaultTaxAmount} - ref={textInput} + ref={(e) => (textInput.current = e)} isCurrencyPressable={false} onSubmitButtonPress={onSubmit} isEditing From 860998b443d777995d56105db09e346b25ed7196 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 5 Apr 2024 01:50:58 +0000 Subject: [PATCH 108/127] Update version to 1.4.60-8 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 098f8d5fc224..0ceafe37dc47 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046007 - versionName "1.4.60-7" + versionCode 1001046008 + versionName "1.4.60-8" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a98f3eb90d63..f956b21669c6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.60.7 + 1.4.60.8 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 91e8a4657385..57f4b843936d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.60.7 + 1.4.60.8 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index d629dd873734..dd6b04f6eb21 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.60 CFBundleVersion - 1.4.60.7 + 1.4.60.8 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index d5e2fcef2392..578550012798 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.60-7", + "version": "1.4.60-8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.60-7", + "version": "1.4.60-8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 35a89ae364eb..873d901b0024 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.60-7", + "version": "1.4.60-8", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 8baf9593d1eb0c3a018bb3de75affaf3d79d8ea9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 5 Apr 2024 11:45:30 +0800 Subject: [PATCH 109/127] 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 110/127] 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 111/127] 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 a0ce1d5ba10d62804f706bf65123e231fd3ab8a4 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 5 Apr 2024 11:05:09 +0530 Subject: [PATCH 112/127] Update optimistic original message --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9d7b6b1d6549..fec64efaac7f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2779,6 +2779,7 @@ function getModifiedExpenseOriginalMessage( if ('taxAmount' in transactionChanges) { originalMessage.oldTaxAmount = TransactionUtils.getTaxAmount(oldTransaction, isFromExpenseReport); originalMessage.taxAmount = transactionChanges?.taxAmount; + originalMessage.currency = TransactionUtils.getCurrency(oldTransaction); } if ('taxCode' in transactionChanges) { From 92200106865b33f86dc95f522623d0d2c436ff68 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 5 Apr 2024 10:41:45 +0000 Subject: [PATCH 113/127] Update version to 1.4.60-9 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0ceafe37dc47..58ec3a1e1310 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046008 - versionName "1.4.60-8" + versionCode 1001046009 + versionName "1.4.60-9" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f956b21669c6..48c18cf62213 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.60.8 + 1.4.60.9 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 57f4b843936d..59e4044fd085 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.60.8 + 1.4.60.9 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index dd6b04f6eb21..caf9d3c95ccb 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.60 CFBundleVersion - 1.4.60.8 + 1.4.60.9 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 578550012798..6fc21583c1b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.60-8", + "version": "1.4.60-9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.60-8", + "version": "1.4.60-9", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 873d901b0024..3c142ab9013a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.60-8", + "version": "1.4.60-9", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 82b33e24c2353b823ed69ebff12c162dca501333 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Fri, 5 Apr 2024 14:02:58 +0200 Subject: [PATCH 114/127] Revert "Replace OptionsSelector with SelectionList - part 1" --- src/components/SelectionList/BaseListItem.tsx | 8 --- .../SelectionList/BaseSelectionList.tsx | 26 +++------- src/components/SelectionList/types.ts | 19 ++----- src/components/TagPicker/index.tsx | 21 +++++--- src/components/WorkspaceSwitcherButton.tsx | 3 -- src/libs/OptionsListUtils.ts | 16 +++--- src/pages/EditReportFieldDropdownPage.tsx | 29 ++++++----- src/pages/WorkspaceSwitcherPage.tsx | 49 +++++++++++++------ .../ShareLogList/BaseShareLogList.tsx | 49 ++++++++++--------- tests/unit/OptionsListUtilsTest.js | 15 ------ 10 files changed, 108 insertions(+), 127 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 5bf8d2f1612f..cd1a40b5ef5d 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -81,14 +81,6 @@ function BaseListItem({ )} - {!item.isSelected && item.brickRoadIndicator && ( - - - - )} {rightHandSideComponentRender()} {FooterComponent} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index dc69dfd39989..8dd7577de779 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -69,11 +69,8 @@ function BaseSelectionList( listHeaderWrapperStyle, isRowMultilineSupported = false, textInputRef, - textInputIconLeft, - sectionTitleStyles, headerMessageStyle, shouldHideListOnInitialRender = true, - textInputAutoFocus = true, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -82,7 +79,7 @@ function BaseSelectionList( const listRef = useRef>>(null); const innerTextInputRef = useRef(null); const focusTimeoutRef = useRef(null); - const shouldShowTextInput = !!textInputLabel || !!textInputIconLeft; + const shouldShowTextInput = !!textInputLabel; const shouldShowSelectAll = !!onSelectAll; const activeElementRole = useActiveElementRole(); const isFocused = useIsFocused(); @@ -313,7 +310,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} ); @@ -380,9 +377,6 @@ 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) { @@ -397,7 +391,7 @@ function BaseSelectionList( } clearTimeout(focusTimeoutRef.current); }; - }, [shouldShowTextInput, textInputAutoFocus]), + }, [shouldShowTextInput]), ); const prevTextInputValue = usePrevious(textInputValue); @@ -500,12 +494,8 @@ function BaseSelectionList( return; } - if (typeof textInputRef === 'function') { - textInputRef(element as RNTextInput); - } else { - // eslint-disable-next-line no-param-reassign - textInputRef.current = element as RNTextInput; - } + // eslint-disable-next-line no-param-reassign + textInputRef.current = element as RNTextInput; }} label={textInputLabel} accessibilityLabel={textInputLabel} @@ -518,7 +508,6 @@ function BaseSelectionList( inputMode={inputMode} selectTextOnFocus spellCheck={false} - iconLeft={textInputIconLeft} onSubmitEditing={selectFocusedOption} blurOnSubmit={!!flattenedSections.allOptions.length} isLoading={isLoadingNewOptions} @@ -526,10 +515,7 @@ function BaseSelectionList( /> )} - - {/* 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 && ( {headerMessage} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index f4b1b990811f..e401dd5456b2 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,12 +1,10 @@ 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'; @@ -112,8 +110,6 @@ type ListItem = { /** The search value from the selection list */ searchText?: string | null; - - brickRoadIndicator?: BrickRoad | '' | null; }; type ListItemProps = CommonListItemProps & { @@ -218,12 +214,6 @@ 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; @@ -231,7 +221,7 @@ type BaseSelectionListProps = Partial & { inputMode?: InputModeOptions; /** Item `keyForList` to focus initially */ - initiallyFocusedOptionKey?: string | null; + initiallyFocusedOptionKey?: string; /** Callback to fire when the list is scrolled */ onScroll?: () => void; @@ -282,7 +272,7 @@ type BaseSelectionListProps = Partial & { disableKeyboardShortcuts?: boolean; /** Styles to apply to SelectionList container */ - containerStyle?: StyleProp; + containerStyle?: ViewStyle; /** Whether keyboard is visible on the screen */ isKeyboardShown?: boolean; @@ -306,10 +296,7 @@ type BaseSelectionListProps = Partial & { isRowMultilineSupported?: boolean; /** Ref for textInput */ - textInputRef?: MutableRefObject | ((ref: TextInput | null) => void); - - /** Styles for the section title */ - sectionTitleStyles?: StyleProp; + textInputRef?: MutableRefObject; /** * 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 8287f9b56be8..54ad016173b7 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -2,8 +2,7 @@ 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 OptionsSelector from '@components/OptionsSelector'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -101,15 +100,21 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; return ( - (); const pressableRef = useRef(null); @@ -45,13 +44,11 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { { pressableRef?.current?.blur(); interceptAnonymousUser(() => { - pressableRef.current?.blur(); Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); }); }} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 063bfa5f1bc8..3acbd9232c87 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1105,18 +1105,16 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Array>, selectedOptions?: SelectedTagOption[]): Option[] { +function getTagsOptions(tags: Array>): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); - return { text: cleanedName, keyForList: tag.name, searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, - isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), }; }); } @@ -1148,7 +1146,7 @@ function getTagListSections( // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagOptions), }); return tagSections; @@ -1161,7 +1159,7 @@ function getTagListSections( // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags, selectedOptions), + data: getTagsOptions(searchTags), }); return tagSections; @@ -1172,7 +1170,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags, selectedOptions), + data: getTagsOptions(enabledTags), }); return tagSections; @@ -1197,7 +1195,7 @@ function getTagListSections( // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagOptions), }); } @@ -1208,7 +1206,7 @@ function getTagListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), + data: getTagsOptions(cutRecentlyUsedTags), }); } @@ -1216,7 +1214,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags, selectedOptions), + data: getTagsOptions(filteredTags), }); return tagSections; 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} /> )} diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 406447d04c05..6f077f764474 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -7,10 +7,9 @@ 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'; @@ -58,6 +57,7 @@ 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,6 +105,11 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const {policyID} = option; + if (policyID) { + setSelectedOption(option); + } else { + setSelectedOption(undefined); + } setActiveWorkspaceID(policyID); Navigation.goBack(); if (policyID !== activeWorkspaceID) { @@ -124,7 +129,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { .map((policy) => ({ text: policy?.name, policyID: policy?.id, - isSelected: policy?.id === activeWorkspaceID, brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), icons: [ { @@ -138,7 +142,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { keyForList: policy?.id, isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), })); - }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline, activeWorkspaceID]); + }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline]); const filteredAndSortedUserWorkspaces = useMemo( () => @@ -232,20 +236,28 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { {usersWorkspaces.length > 0 ? ( - = CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH} onChangeText={setSearchTerm} + selectedOptions={selectedOption ? [selectedOption] : []} onSelectRow={selectPolicy} shouldPreventDefaultFocusOnSelectRow headerMessage={headerMessage} - containerStyle={[styles.pt0, styles.mt0]} - textInputIconLeft={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? MagnifyingGlass : undefined} - initiallyFocusedOptionKey={activeWorkspaceID} - textInputAutoFocus={false} + 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} /> ) : ( @@ -257,6 +269,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { setSearchTerm, searchTerm, selectPolicy, + selectedOption, styles, theme.textSupporting, translate, @@ -268,6 +281,14 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { ], ); + useEffect(() => { + if (!activeWorkspaceID) { + return; + } + const optionToSet = usersWorkspaces.find((option) => option.policyID === activeWorkspaceID); + setSelectedOption(optionToSet); + }, [activeWorkspaceID, usersWorkspaces]); + return ( { + const attachLogToReport = (option: Report) => { if (!option.reportID) { return; } @@ -111,24 +110,30 @@ function BaseShareLogList({betas, onAttachLogToReport}: BaseShareLogListProps) { testID={BaseShareLogList.displayName} includeSafeAreaPaddingBottom={false} > - Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} - /> - - - + {({safeAreaPaddingBottomStyle}) => ( + <> + Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} + /> + + + + + )} ); } diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 981f5285c88d..d590236e5256 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1130,7 +1130,6 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, - isSelected: false, }, { text: 'HR', @@ -1138,7 +1137,6 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, - isSelected: false, }, { text: 'Medical', @@ -1146,7 +1144,6 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, - isSelected: false, }, ], }, @@ -1162,7 +1159,6 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, - isSelected: false, }, ], }, @@ -1231,7 +1227,6 @@ describe('OptionsListUtils', () => { searchText: 'Medical', tooltipText: 'Medical', isDisabled: false, - isSelected: true, }, ], }, @@ -1245,7 +1240,6 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, - isSelected: false, }, ], }, @@ -1260,7 +1254,6 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, - isSelected: false, }, { text: 'Benefits', @@ -1268,7 +1261,6 @@ describe('OptionsListUtils', () => { searchText: 'Benefits', tooltipText: 'Benefits', isDisabled: false, - isSelected: false, }, { text: 'Cleaning', @@ -1276,7 +1268,6 @@ describe('OptionsListUtils', () => { searchText: 'Cleaning', tooltipText: 'Cleaning', isDisabled: false, - isSelected: false, }, { text: 'Food', @@ -1284,7 +1275,6 @@ describe('OptionsListUtils', () => { searchText: 'Food', tooltipText: 'Food', isDisabled: false, - isSelected: false, }, { text: 'HR', @@ -1292,7 +1282,6 @@ describe('OptionsListUtils', () => { searchText: 'HR', tooltipText: 'HR', isDisabled: false, - isSelected: false, }, { text: 'Software', @@ -1300,7 +1289,6 @@ describe('OptionsListUtils', () => { searchText: 'Software', tooltipText: 'Software', isDisabled: false, - isSelected: false, }, { text: 'Taxes', @@ -1308,7 +1296,6 @@ describe('OptionsListUtils', () => { searchText: 'Taxes', tooltipText: 'Taxes', isDisabled: false, - isSelected: false, }, ], }, @@ -1324,7 +1311,6 @@ describe('OptionsListUtils', () => { searchText: 'Accounting', tooltipText: 'Accounting', isDisabled: false, - isSelected: false, }, { text: 'Cleaning', @@ -1332,7 +1318,6 @@ describe('OptionsListUtils', () => { searchText: 'Cleaning', tooltipText: 'Cleaning', isDisabled: false, - isSelected: false, }, ], }, From 380816f2927d9de904453c25a8911e23591408e1 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 5 Apr 2024 17:44:51 +0530 Subject: [PATCH 115/127] Safely access taxes properties --- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils.ts | 8 ++++---- src/pages/EditRequestPage.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fec64efaac7f..978685a9be2d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2783,7 +2783,7 @@ function getModifiedExpenseOriginalMessage( } if ('taxCode' in transactionChanges) { - originalMessage.oldTaxRate = policy?.taxRates?.taxes[TransactionUtils.getTaxCode(oldTransaction)].value; + originalMessage.oldTaxRate = policy?.taxRates?.taxes[TransactionUtils.getTaxCode(oldTransaction)]?.value; originalMessage.taxRate = transactionChanges?.taxCode && policy?.taxRates?.taxes[transactionChanges?.taxCode].value; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index af28133ecf3f..4244f20d0bc3 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -625,7 +625,7 @@ function getEnabledTaxRateCount(options: TaxRates) { */ function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction?: Transaction) { const defaultTaxKey = taxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${Localize.translateLocal('common.default')}`) || ''; + const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey]?.name} (${taxRates.taxes[defaultTaxKey]?.value}) • ${Localize.translateLocal('common.default')}`) || ''; return transaction?.taxRate?.text ?? defaultTaxName; } @@ -633,9 +633,9 @@ function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction?: Transact * Gets the tax name */ function getTaxName(taxes: TaxRates, transactionTaxCode: string) { - const taxName = `${taxes[transactionTaxCode].name}`; - const taxValue = `${taxes[transactionTaxCode].value}`; - return transactionTaxCode ? `${taxName} (${taxValue})` : ''; + const taxName = taxes[transactionTaxCode]?.name ?? ''; + const taxValue = taxes[transactionTaxCode]?.value ?? ''; + return transactionTaxCode && taxName && taxValue ? `${taxName} (${taxValue})` : ''; } export { diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index ec49e32a5f0f..cfb1c18d76f1 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -73,7 +73,7 @@ const defaultProps = { }; const getTaxAmount = (transactionAmount, transactionTaxCode, taxRates) => { - const percentage = (transactionTaxCode ? taxRates.taxes[transactionTaxCode].value : taxRates.defaultValue) || ''; + const percentage = (transactionTaxCode ? taxRates.taxes[transactionTaxCode]?.value : taxRates.defaultValue) || ''; return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transactionAmount))); }; From 17c39e4a026d97c81141bd90bafdd3f14163ba7e Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 5 Apr 2024 18:14:52 +0530 Subject: [PATCH 116/127] Fix lint: use conditional check --- src/pages/EditRequestPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index cfb1c18d76f1..bef826300af2 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -73,7 +73,7 @@ const defaultProps = { }; const getTaxAmount = (transactionAmount, transactionTaxCode, taxRates) => { - const percentage = (transactionTaxCode ? taxRates.taxes[transactionTaxCode]?.value : taxRates.defaultValue) || ''; + const percentage = (transactionTaxCode && taxRates.taxes[transactionTaxCode] ? taxRates.taxes[transactionTaxCode].value : taxRates.defaultValue) || ''; return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transactionAmount))); }; From 51c039d6632d49cacf4334d88d8276e9c8509fec Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 5 Apr 2024 13:45:07 +0000 Subject: [PATCH 117/127] Update version to 1.4.60-10 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 58ec3a1e1310..b840e0e674c6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046009 - versionName "1.4.60-9" + versionCode 1001046010 + versionName "1.4.60-10" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 48c18cf62213..3883e2d10a1e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.60.9 + 1.4.60.10 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 59e4044fd085..499494a40920 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.60.9 + 1.4.60.10 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index caf9d3c95ccb..60147cd29c67 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.60 CFBundleVersion - 1.4.60.9 + 1.4.60.10 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 6fc21583c1b8..5646844a6061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.60-9", + "version": "1.4.60-10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.60-9", + "version": "1.4.60-10", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3c142ab9013a..d050e8c754dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.60-9", + "version": "1.4.60-10", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 7dcb3e0eb79ed36c1cc7622fbc6375c7e084a917 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 5 Apr 2024 14:15:03 +0000 Subject: [PATCH 118/127] Update version to 1.4.60-11 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b840e0e674c6..04e613aca00d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046010 - versionName "1.4.60-10" + versionCode 1001046011 + versionName "1.4.60-11" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3883e2d10a1e..0560f866d954 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.60.10 + 1.4.60.11 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 499494a40920..e7b90623fcd1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.60.10 + 1.4.60.11 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 60147cd29c67..34373a021d7e 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.60 CFBundleVersion - 1.4.60.10 + 1.4.60.11 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 5646844a6061..36087251db0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.60-10", + "version": "1.4.60-11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.60-10", + "version": "1.4.60-11", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d050e8c754dc..da76ada8740f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.60-10", + "version": "1.4.60-11", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 0e4053bd1f6be807457cf076ef40d3b27ffc4059 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 5 Apr 2024 21:25:45 +0700 Subject: [PATCH 119/127] validate tax amount in edit flow --- src/pages/iou/steps/MoneyRequestAmountForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index 755da80fb93f..ca7bb694599a 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -232,7 +232,7 @@ function MoneyRequestAmountForm( */ const submitAndNavigateToNextPage = useCallback(() => { // Skip the check for tax amount form as 0 is a valid input - if (!isTaxAmountForm && isAmountInvalid(currentAmount)) { + if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { setFormError('iou.error.invalidAmount'); return; } From 5ba54d19e6fb014f0ce4ae3e89b8ef1e3ac52301 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 5 Apr 2024 14:48:05 +0000 Subject: [PATCH 120/127] Update version to 1.4.60-12 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 04e613aca00d..f178a17e863d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046011 - versionName "1.4.60-11" + versionCode 1001046012 + versionName "1.4.60-12" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 0560f866d954..c7b36f67d83c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.60.11 + 1.4.60.12 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e7b90623fcd1..e254e49e9a40 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.60.11 + 1.4.60.12 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 34373a021d7e..5d58cf3ec7cb 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.60 CFBundleVersion - 1.4.60.11 + 1.4.60.12 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 36087251db0a..b961f4db57ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.60-11", + "version": "1.4.60-12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.60-11", + "version": "1.4.60-12", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index da76ada8740f..029fdc8c87ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.60-11", + "version": "1.4.60-12", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 6fd4517e3b5598393d038cc4c44ce92899ae362c Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 5 Apr 2024 17:53:11 +0300 Subject: [PATCH 121/127] changed variable names --- src/libs/ReportActionComposeFocusManager.ts | 6 +++--- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 4 ++-- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 7e387d807229..11c1fd04329f 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -3,7 +3,7 @@ import type {TextInput} from 'react-native'; import ROUTES from '@src/ROUTES'; import Navigation from './Navigation/Navigation'; -type FocusCallback = (shouldFocusForNative?: boolean) => void; +type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void; const composerRef = React.createRef(); const editComposerRef = React.createRef(); @@ -29,7 +29,7 @@ function onComposerFocus(callback: FocusCallback | null, isMainComposer = false) /** * Request focus on the ReportActionComposer */ -function focus(shouldFocusForNative?: boolean) { +function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) { /** Do not trigger the refocusing when the active route is not the report route, */ if (!Navigation.isActiveRoute(ROUTES.REPORT_WITH_ID.getRoute(Navigation.getTopmostReportId() ?? ''))) { return; @@ -40,7 +40,7 @@ function focus(shouldFocusForNative?: boolean) { return; } - mainComposerFocusCallback(shouldFocusForNative); + mainComposerFocusCallback(shouldFocusForNonBlurInputOnTapOutside); return; } diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 8294ba8d4847..4aba9e43b1c0 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -202,8 +202,8 @@ const ContextMenuActions: ContextMenuAction[] = [ if (closePopover) { hideContextMenu(false, () => { InteractionManager.runAfterInteractions(() => { - // Normally the focus callback of the main composer doesn't focus for natives, - // so we need to pass true here to allow focusing for natives too. + // Normally the focus callback of the main composer doesn't focus when willBlurTextInputOnTapOutside + // is false, so we need to pass true here to override this condition. ReportActionComposeFocusManager.focus(true); }); Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index c2001229d89b..f8147dfda81d 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -591,8 +591,8 @@ function ComposerWithSuggestions( const setUpComposeFocusManager = useCallback(() => { // This callback is used in the contextMenuActions to manage giving focus back to the compose input. - ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNative = false) => { - if ((!willBlurTextInputOnTapOutside && !shouldFocusForNative) || !isFocused) { + ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => { + if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused) { return; } From 0430453ce69600f3fe388d6f75d2159596aa1d61 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga Date: Fri, 5 Apr 2024 09:14:16 -0700 Subject: [PATCH 122/127] fix: display text --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 55a4c586716a..041c90146ec0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1915,7 +1915,7 @@ export default { subtitle: 'Set up custom fields for spend.', }, connections: { - title: 'Connections', + title: 'Accounting', subtitle: 'Sync your chart of accounts and more.', }, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 5956f1457005..ef6ab4a66759 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1942,7 +1942,7 @@ export default { subtitle: 'Configura campos personalizados para los gastos.', }, connections: { - title: 'Conexión', + title: 'Contabilidad', subtitle: 'Sincroniza tu plan de cuentas y otras opciones.', }, }, From 446f8ff66c43a459717ce3207be09f72a98b6ec6 Mon Sep 17 00:00:00 2001 From: caitlinwhite1 Date: Fri, 5 Apr 2024 11:48:40 -0500 Subject: [PATCH 123/127] Update redirects.csv redirect billing owner article that's going to be deleted --- docs/redirects.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index d58919512abd..ca87274a2fbb 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -79,3 +79,4 @@ https://help.expensify.com/articles/expensify-classic/workspace-and-domain-setti https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles,https://help.expensify.com/articles/expensify-classic/workspaces/Change-member-workspace-roles https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/tax-tracking,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Apply-Tax https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles.html,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/ +https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Owner,https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account From 1460677b82e832bfd8ac01317016acd6ca484c95 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 6 Apr 2024 00:52:29 +0800 Subject: [PATCH 124/127] 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 25361223ea0d10eec12cae2d6221511998e97b3d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 6 Apr 2024 01:04:14 +0800 Subject: [PATCH 125/127] move optionlistcontextprovider to auth screen so it's resetted when sign out --- src/App.tsx | 2 - .../Navigation/AppNavigator/AuthScreens.tsx | 251 +++++++++--------- 2 files changed, 127 insertions(+), 126 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/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 295daa1938e7..fde0202d3d2f 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -2,6 +2,7 @@ import React, {memo, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx, {withOnyx} from 'react-native-onyx'; +import OptionsListContextProvider from '@components/OptionListContextProvider'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -266,130 +267,132 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie }, []); return ( - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + ); } From 18bc7b40ea975d4ba0f9d3c12ac2f4b36b50b4f9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 6 Apr 2024 02:12:37 +0800 Subject: [PATCH 126/127] 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 c82ec7e953d64e2c01e22af2bdc06870618058f6 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 5 Apr 2024 19:08:38 +0000 Subject: [PATCH 127/127] Update version to 1.4.60-13 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f178a17e863d..bc08cbbeed21 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046012 - versionName "1.4.60-12" + versionCode 1001046013 + versionName "1.4.60-13" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c7b36f67d83c..0db15a68744f 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.60.12 + 1.4.60.13 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e254e49e9a40..e133f93aa125 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.60.12 + 1.4.60.13 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5d58cf3ec7cb..12e153cd1f3b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.60 CFBundleVersion - 1.4.60.12 + 1.4.60.13 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index b961f4db57ac..391b13e99305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.60-12", + "version": "1.4.60-13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.60-12", + "version": "1.4.60-13", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 029fdc8c87ff..5e00815f1e3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.60-12", + "version": "1.4.60-13", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",