diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a695c0acf942..a2aadc331f19 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -48,12 +48,12 @@ jobs: git fetch origin "$BASELINE_BRANCH" --no-tags --depth=1 git switch "$BASELINE_BRANCH" npm install --force - npx reassure --baseline + NODE_OPTIONS=--experimental-vm-modules npx reassure --baseline git switch --force --detach - git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours git checkout --ours . npm install --force - npx reassure --branch + NODE_OPTIONS=--experimental-vm-modules npx reassure --branch - name: Validate output.json id: validateReassureOutput diff --git a/android/app/build.gradle b/android/app/build.gradle index 823974918b2a..99d0f3e8fa0a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009000407 - versionName "9.0.4-7" + versionCode 1009000503 + versionName "9.0.5-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/circular-arrow-backwards.svg b/assets/images/circular-arrow-backwards.svg new file mode 100644 index 000000000000..209c0aea5fa7 --- /dev/null +++ b/assets/images/circular-arrow-backwards.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a6b9d8632061..daa62045b1a8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.4 + 9.0.5 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.4.7 + 9.0.5.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ce34b27d72e3..c0106ae0c33d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.4 + 9.0.5 CFBundleSignature ???? CFBundleVersion - 9.0.4.7 + 9.0.5.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 9b89c5e2790f..f9ea0de27fd0 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.4 + 9.0.5 CFBundleVersion - 9.0.4.7 + 9.0.5.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 8bb3deda6ca7..a2ceab335286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.4-7", + "version": "9.0.5-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.4-7", + "version": "9.0.5-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6285c2ff077e..1301c59e83bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.4-7", + "version": "9.0.5-3", "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.", diff --git a/src/CONST.ts b/src/CONST.ts index 8ecdadefc4e9..2e2608d773ae 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -719,7 +719,7 @@ const CONST = { TASK_EDITED: 'TASKEDITED', TASK_REOPENED: 'TASKREOPENED', TRIPPREVIEW: 'TRIPPREVIEW', - UNAPPROVED: 'UNAPPROVED', // OldDot Action + UNAPPROVED: 'UNAPPROVED', UNHOLD: 'UNHOLD', UNSHARE: 'UNSHARE', // OldDot Action UPDATE_GROUP_CHAT_MEMBER_ROLE: 'UPDATEGROUPCHATMEMBERROLE', @@ -2335,6 +2335,7 @@ const CONST = { PRIVATE_NOTES: 'privateNotes', DELETE: 'delete', MARK_AS_INCOMPLETE: 'markAsIncomplete', + UNAPPROVE: 'unapprove', }, EDIT_REQUEST_FIELD: { AMOUNT: 'amount', diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts index 7af327d35ac4..8c016032e143 100644 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -9,6 +9,10 @@ const SCREENS_WITH_AUTOFOCUS: string[] = [ SCREENS.PRIVATE_NOTES.EDIT, SCREENS.SETTINGS.PROFILE.STATUS, SCREENS.SETTINGS.PROFILE.PRONOUNS, + SCREENS.REPORT_SETTINGS.ROOT, + SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES, + SCREENS.REPORT_PARTICIPANTS.ROOT, + SCREENS.ROOM_MEMBERS_ROOT, SCREENS.NEW_TASK.DETAILS, SCREENS.MONEY_REQUEST.CREATE, SCREENS.SIGN_IN_ROOT, diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index a0d7a5cb8883..487df5594212 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -42,6 +42,7 @@ import ChatBubbles from '@assets/images/chatbubbles.svg'; import CheckCircle from '@assets/images/check-circle.svg'; import CheckmarkCircle from '@assets/images/checkmark-circle.svg'; import Checkmark from '@assets/images/checkmark.svg'; +import CircularArrowBackwards from '@assets/images/circular-arrow-backwards.svg'; import Close from '@assets/images/close.svg'; import ClosedSign from '@assets/images/closed-sign.svg'; import Coins from '@assets/images/coins.svg'; @@ -201,6 +202,7 @@ export { Wrench, BackArrow, Bank, + CircularArrowBackwards, Bill, Bell, BellSlash, diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 7e6682492eb2..45ac5dc80a1a 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -531,6 +531,18 @@ function MoneyRequestConfirmationList({ ], ); + const shouldDisableParticipant = (participant: Participant): boolean => { + if (ReportUtils.isDraftReport(participant.reportID)) { + return true; + } + + if (!participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1)) { + return true; + } + + return false; + }; + const sections = useMemo(() => { const options: Array> = []; if (isTypeSplit) { @@ -553,7 +565,7 @@ function MoneyRequestConfirmationList({ const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, isSelected: false, - isDisabled: !participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), + isInteractive: !shouldDisableParticipant(participant), })); options.push({ title: translate('common.to'), diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 154f5c1e1cd3..0f97a3c4414f 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -109,16 +109,18 @@ function PopoverMenu({ const selectedItemIndex = useRef(null); const [currentMenuItems, setCurrentMenuItems] = useState(menuItems); + const currentMenuItemsFocusedIndex = currentMenuItems?.findIndex((option) => option.isSelected); const [enteredSubMenuIndexes, setEnteredSubMenuIndexes] = useState([]); - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: currentMenuItems.length - 1, isActive: isVisible}); + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: currentMenuItemsFocusedIndex, maxIndex: currentMenuItems.length - 1, isActive: isVisible}); const selectItem = (index: number) => { const selectedItem = currentMenuItems[index]; if (selectedItem?.subMenuItems) { setCurrentMenuItems([...selectedItem.subMenuItems]); setEnteredSubMenuIndexes([...enteredSubMenuIndexes, index]); - setFocusedIndex(-1); + const selectedSubMenuItemIndex = selectedItem?.subMenuItems.findIndex((option) => option.isSelected); + setFocusedIndex(selectedSubMenuItemIndex); } else { selectedItemIndex.current = index; onItemSelected(selectedItem, index); diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index 377007d40c54..5237ff486631 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -36,6 +36,7 @@ function GenericPressable( onPressOut, accessible = true, fullDisabled = false, + interactive = true, ...rest }: PressableProps, ref: PressableRef, @@ -67,6 +68,9 @@ function GenericPressable( * Returns the cursor style based on the state of Pressable */ const cursorStyle = useMemo(() => { + if (!interactive) { + return styles.cursorDefault; + } if (shouldUseDisabledCursor) { return styles.cursorDisabled; } @@ -74,7 +78,7 @@ function GenericPressable( return styles.cursorText; } return styles.cursorPointer; - }, [styles, shouldUseDisabledCursor, rest.accessibilityRole, rest.role]); + }, [styles, shouldUseDisabledCursor, rest.accessibilityRole, rest.role, interactive]); const onLongPressHandler = useCallback( (event: GestureResponderEvent) => { @@ -98,7 +102,7 @@ function GenericPressable( const onPressHandler = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { - if (isDisabled) { + if (isDisabled || !interactive) { return; } if (!onPress) { @@ -113,7 +117,7 @@ function GenericPressable( } return onPress(event); }, - [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], + [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled, interactive], ); const voidOnPressHandler = useCallback( diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index 26a2fea42d94..61cb6db8ee76 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -142,6 +142,12 @@ type PressableProps = RNPressableProps & * Specifies if the pressable responder should be disabled */ fullDisabled?: boolean; + + /** + * Whether the menu item should be interactive at all + * e.g., show disabled cursor when disabled + */ + interactive?: boolean; }; type PressableRef = ForwardedRef; diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index c9dc773c8818..99330478c75f 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -82,6 +82,7 @@ function BaseListItem({ onSelectRow(item); }} disabled={isDisabled && !item.isSelected} + interactive={item.isInteractive} accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index a20f0ae18c58..d40a6f0fa225 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -81,6 +81,9 @@ type ListItem = { /** Whether this option is disabled for selection */ isDisabled?: boolean | null; + /** Whether this item should be interactive at all */ + isInteractive?: boolean; + /** List title is bold by default. Use this props to customize it */ isBold?: boolean; diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx index 0958ec148c3d..bfb7e3739d17 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -62,6 +62,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { updatePlaybackSpeed(speed); }, shouldPutLeftPaddingWhenNoIcon: true, + isSelected: currentPlaybackSpeed === speed, })), }); return items; diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index e21e2a77268c..51076b0818d4 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -9,7 +9,8 @@ const defaultEmptyArray: Array = []; function useMarkdownStyle(message: string | null = null, excludeStyles: Array = defaultEmptyArray): MarkdownStyle { const theme = useTheme(); - const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal; + const hasMessageOnlyEmojis = message != null && message.length > 0 && containsOnlyEmojis(message); + const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal; // this map is used to reset the styles that are not needed - passing undefined value can break the native side const nonStylingDefaultValues: Record = useMemo( diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index a90793857293..6059af105a77 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -3,7 +3,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -13,7 +12,6 @@ import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; -type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; type PolicySelector = Pick; type ReportActionsSelector = Array>; @@ -25,56 +23,19 @@ type ReportIDsContextProviderProps = { type ReportIDsContextValue = { orderedReportIDs: string[]; currentReportID: string; + policyMemberAccountIDs: number[]; }; const ReportIDsContext = createContext({ orderedReportIDs: [], currentReportID: '', + policyMemberAccountIDs: [], }); /** * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. */ -const chatReportSelector = (report: OnyxEntry): ChatReportSelector => - (report && { - reportID: report.reportID, - participants: report.participants, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, - }, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }) as ChatReportSelector; - const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => (reportActions && Object.values(reportActions) @@ -118,7 +79,7 @@ function ReportIDsContextProvider({ currentReportIDForTests, }: ReportIDsContextProviderProps) { const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {initialValue: CONST.PRIORITY_MODE.DEFAULT}); - const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: chatReportSelector}); + const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policySelector}); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: reportActionsSelector}); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); @@ -130,7 +91,7 @@ function ReportIDsContextProvider({ const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID; const {activeWorkspaceID} = useActiveWorkspace(); - const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); + const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID), [policies, activeWorkspaceID, accountID]); const getOrderedReportIDs = useCallback( (currentReportID?: string) => @@ -157,15 +118,16 @@ function ReportIDsContextProvider({ // we first generate the list as if there was no current report, then we check if // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. - if (derivedCurrentReportID && !orderedReportIDs.includes(derivedCurrentReportID)) { - return {orderedReportIDs: getOrderedReportIDs(derivedCurrentReportID), currentReportID: derivedCurrentReportID ?? '-1'}; + if (derivedCurrentReportID && derivedCurrentReportID !== '-1' && orderedReportIDs.indexOf(derivedCurrentReportID) === -1) { + return {orderedReportIDs: getOrderedReportIDs(derivedCurrentReportID), currentReportID: derivedCurrentReportID ?? '-1', policyMemberAccountIDs}; } return { orderedReportIDs, currentReportID: derivedCurrentReportID ?? '-1', + policyMemberAccountIDs, }; - }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID]); + }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID, policyMemberAccountIDs]); return {children}; } @@ -175,4 +137,4 @@ function useReportIDs() { } export {ReportIDsContext, ReportIDsContextProvider, policySelector, useReportIDs}; -export type {ChatReportSelector, PolicySelector, ReportActionsSelector}; +export type {PolicySelector, ReportActionsSelector}; diff --git a/src/languages/en.ts b/src/languages/en.ts index e3e080f26201..a72e07701c8a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -814,6 +814,11 @@ export default { removed: 'removed', transactionPending: 'Transaction pending.', chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, + unapprove: 'Unapprove', + unapproveReport: 'Unapprove report', + headsUp: 'Heads up!', + unapproveWithIntegrationWarning: (accountingIntegration: string) => + `This report has already been exported to ${accountingIntegration}. Changes to this report in Expensify may lead to data discrepancies and Expensify Card reconciliation issues. Are you sure you want to unapprove this report?`, }, notificationPreferencesPage: { header: 'Notification preferences', @@ -2759,6 +2764,21 @@ export default { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + connectionName: (integration: ConnectionName) => { + switch (integration) { + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return 'Quickbooks Online'; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return 'Xero'; + case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: + return 'NetSuite'; + case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: + return 'Sage Intacct'; + default: { + return ''; + } + } + }, setup: 'Connect', lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, import: 'Import', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9e104ca9b1bb..c88b273ae05d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -817,6 +817,11 @@ export default { removed: 'eliminó', transactionPending: 'Transacción pendiente.', chooseARate: ({unit}: ReimbursementRateParams) => `Selecciona una tasa de reembolso por ${unit} del espacio de trabajo`, + unapprove: 'Desaprobar', + unapproveReport: 'Anular la aprobación del informe', + headsUp: 'Atención!', + unapproveWithIntegrationWarning: (accountingIntegration: string) => + `Este informe ya se ha exportado a ${accountingIntegration}. Los cambios realizados en este informe en Expensify pueden provocar discrepancias en los datos y problemas de conciliación de la tarjeta Expensify. ¿Está seguro de que desea anular la aprobación de este informe?`, }, notificationPreferencesPage: { header: 'Preferencias de avisos', @@ -2741,6 +2746,21 @@ export default { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + connectionName: (integration: ConnectionName) => { + switch (integration) { + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return 'Quickbooks Online'; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return 'Xero'; + case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: + return 'NetSuite'; + case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: + return 'Sage Intacct'; + default: { + return ''; + } + } + }, setup: 'Configurar', lastSync: (relativeDate: string) => `Recién sincronizado ${relativeDate}`, import: 'Importar', diff --git a/src/libs/API/parameters/UnapproveExpenseReportParams.ts b/src/libs/API/parameters/UnapproveExpenseReportParams.ts new file mode 100644 index 000000000000..ba25424aeda6 --- /dev/null +++ b/src/libs/API/parameters/UnapproveExpenseReportParams.ts @@ -0,0 +1,6 @@ +type UnapproveExpenseReportParams = { + reportID: string; + reportActionID: string; +}; + +export default UnapproveExpenseReportParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a49cb68fd04f..096e59f399f6 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -153,6 +153,7 @@ export type {default as CreateDistanceRequestParams} from './CreateDistanceReque export type {default as StartSplitBillParams} from './StartSplitBillParams'; export type {default as SendMoneyParams} from './SendMoneyParams'; export type {default as ApproveMoneyRequestParams} from './ApproveMoneyRequestParams'; +export type {default as UnapproveExpenseReportParams} from './UnapproveExpenseReportParams'; export type {default as EditMoneyRequestParams} from './EditMoneyRequestParams'; export type {default as ReplaceReceiptParams} from './ReplaceReceiptParams'; export type {default as SubmitReportParams} from './SubmitReportParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index dae65e7792bc..a1a5b91d7d99 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -172,6 +172,7 @@ const WRITE_COMMANDS = { SEND_MONEY_ELSEWHERE: 'SendMoneyElsewhere', SEND_MONEY_WITH_WALLET: 'SendMoneyWithWallet', APPROVE_MONEY_REQUEST: 'ApproveMoneyRequest', + UNAPPROVE_EXPENSE_REPORT: 'UnapproveExpenseReport', EDIT_MONEY_REQUEST: 'EditMoneyRequest', REPLACE_RECEIPT: 'ReplaceReceipt', SUBMIT_REPORT: 'SubmitReport', @@ -432,6 +433,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SEND_MONEY_ELSEWHERE]: Parameters.SendMoneyParams; [WRITE_COMMANDS.SEND_MONEY_WITH_WALLET]: Parameters.SendMoneyParams; [WRITE_COMMANDS.APPROVE_MONEY_REQUEST]: Parameters.ApproveMoneyRequestParams; + [WRITE_COMMANDS.UNAPPROVE_EXPENSE_REPORT]: Parameters.UnapproveExpenseReportParams; [WRITE_COMMANDS.EDIT_MONEY_REQUEST]: Parameters.EditMoneyRequestParams; [WRITE_COMMANDS.REPLACE_RECEIPT]: Parameters.ReplaceReceiptParams; [WRITE_COMMANDS.SUBMIT_REPORT]: Parameters.SubmitReportParams; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index dda5427e9c9f..a85db2bc28d8 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -1,15 +1,16 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {RateAndUnit} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {LastSelectedDistanceRates, OnyxInputOrEntry, Report} from '@src/types/onyx'; +import type {LastSelectedDistanceRates, OnyxInputOrEntry} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; +import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; type MileageRate = { @@ -28,13 +29,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - const METERS_TO_KM = 0.001; // 1 kilometer is 1000 meters const METERS_TO_MILES = 0.000621371; // There are approximately 0.000621371 miles in a meter @@ -251,6 +245,7 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { * Returns custom unit rate ID for the distance transaction */ function getCustomUnitRateID(reportID: string) { + const allReports = ReportConnection.getAllReports(); const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID ?? '-1'); diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index efcba4c23204..38562edb7704 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -2,12 +2,13 @@ import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyTagList, Report, ReportAction} from '@src/types/onyx'; +import type {PolicyTagList, ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; +import * as ReportConnection from './ReportConnection'; import * as TransactionUtils from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; @@ -23,13 +24,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - /** * Builds the partial message fragment for a modified field on the expense. */ @@ -116,7 +110,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr return ''; } const reportActionOriginalMessage = ReportActionsUtils.getOriginalMessage(reportAction); - const policyID = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1'; + const policyID = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1'; const removalFragments: string[] = []; const setFragments: string[] = []; diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index e9bfb7227403..15d4ac6e4b31 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,10 +1,10 @@ import {findFocusedRoute} from '@react-navigation/core'; import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native'; import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import Log from '@libs/Log'; import {isCentralPaneName, removePolicyIDParamFromState} from '@libs/NavigationUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -35,13 +35,6 @@ let pendingRoute: Route | null = null; let shouldPopAllStateOnUP = false; -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - /** * Inform the navigation that next time user presses UP we should pop all the state back to LHN. */ @@ -69,7 +62,7 @@ const dismissModal = (reportID?: string, ref = navigationRef) => { originalDismissModal(ref); return; } - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; originalDismissModalWithReport({reportID, ...report}, ref); }; // Re-exporting the closeRHPFlow here to fill in default value for navigationRef. The closeRHPFlow isn't defined in this file to avoid cyclic dependencies. diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts index 18099f157d6a..d60e66f8d535 100644 --- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts +++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; @@ -7,13 +6,14 @@ import Navigation from '@libs/Navigation/Navigation'; import type {ReportActionPushNotificationData} from '@libs/Notification/PushNotification/NotificationType'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OnyxUpdatesFromServer, Report} from '@src/types/onyx'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PushNotification from '..'; @@ -28,13 +28,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - function getLastUpdateIDAppliedToClient(): Promise { return new Promise((resolve) => { Onyx.connect({ @@ -82,7 +75,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati Log.info('[PushNotification] Navigating to report', false, {reportID, reportActionID}); const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); diff --git a/src/libs/OnyxAwareParser.ts b/src/libs/OnyxAwareParser.ts index c058775341c2..51ea39ef972a 100644 --- a/src/libs/OnyxAwareParser.ts +++ b/src/libs/OnyxAwareParser.ts @@ -1,23 +1,12 @@ import {ExpensiMark} from 'expensify-common'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import * as ReportConnection from './ReportConnection'; const parser = new ExpensiMark(); -const reportIDToNameMap: Record = {}; const accountIDToNameMap: Record = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (report) => { - if (!report) { - return; - } - - reportIDToNameMap[report.reportID] = report.reportName ?? report.displayName ?? report.reportID; - }, -}); - Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (personalDetailsList) => { @@ -37,7 +26,11 @@ function parseHtmlToMarkdown( accountIDToName?: Record, cacheVideoAttributes?: (videoSource: string, videoAttrs: string) => void, ): string { - return parser.htmlToMarkdown(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap, cacheVideoAttributes}); + return parser.htmlToMarkdown(html, { + reportIDToName: reportIDToName ?? ReportConnection.getAllReportsNameMap(), + accountIDToName: accountIDToName ?? accountIDToNameMap, + cacheVideoAttributes, + }); } function parseHtmlToText( @@ -46,7 +39,7 @@ function parseHtmlToText( accountIDToName?: Record, cacheVideoAttributes?: (videoSource: string, videoAttrs: string) => void, ): string { - return parser.htmlToText(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap, cacheVideoAttributes}); + return parser.htmlToText(html, {reportIDToName: reportIDToName ?? ReportConnection.getAllReportsNameMap(), accountIDToName: accountIDToName ?? accountIDToNameMap, cacheVideoAttributes}); } export {parseHtmlToMarkdown, parseHtmlToText}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fc73c85b0354..2c96c526ceea 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -53,6 +53,7 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionUtils from './ReportActionsUtils'; +import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; @@ -339,13 +340,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - let allReportsDraft: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_DRAFT, @@ -357,6 +351,7 @@ Onyx.connect({ * Get the report or draft report given a reportID */ function getReportOrDraftReport(reportID: string | undefined): OnyxEntry { + const allReports = ReportConnection.getAllReports(); if (!allReports && !allReportsDraft) { return undefined; } diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 8bdf0cb1d5fe..9beb3d382696 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -32,11 +32,16 @@ Onyx.connect({ }, }); +const hiddenTranslation = Localize.translateLocal('common.hidden'); +const youTranslation = Localize.translateLocal('common.you').toLowerCase(); + +const regexMergedAccount = new RegExp(CONST.REGEX.MERGED_ACCOUNT_PREFIX); + function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string { let displayName = passedPersonalDetails?.displayName ?? ''; // If the displayName starts with the merged account prefix, remove it. - if (new RegExp(CONST.REGEX.MERGED_ACCOUNT_PREFIX).test(displayName)) { + if (regexMergedAccount.test(displayName)) { // Remove the merged account prefix from the displayName. displayName = displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); } @@ -48,7 +53,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - allReports = reports; - }, -}); - let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, @@ -378,7 +370,7 @@ function getCombinedReportActions( // Filter out request money actions because we don't want to show any preview actions for one transaction reports const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const isSelfDM = report?.chatType === CONST.REPORT.CHAT_TYPE.SELF_DM; // Filter out request and send money request actions because we don't want to show any preview actions for one transaction reports const filteredReportActions = [...reportActions, ...filteredTransactionThreadReportActions].filter((action) => { @@ -672,8 +664,13 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo } function getLastVisibleAction(reportID: string, actionsToMerge: OnyxCollection | OnyxCollectionInputValue = {}): OnyxEntry { - const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)); - const visibleReportActions = Object.values(reportActions ?? {}).filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action)); + let reportActions: Array = []; + if (!_.isEmpty(actionsToMerge)) { + reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)); + } else { + reportActions = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); + } + const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -835,7 +832,7 @@ function getMostRecentReportActionLastModified(): string { // We might not have actions so we also look at the report objects to see if any have a lastVisibleActionLastModified that is more recent. We don't need to get // any reports that have been updated before either a recently updated report or reportAction as we should be up to date on these - Object.values(allReports ?? {}).forEach((report) => { + Object.values(ReportConnection.getAllReports() ?? {}).forEach((report) => { const reportLastVisibleActionLastModified = report?.lastVisibleActionLastModified ?? report?.lastVisibleActionCreated; if (!reportLastVisibleActionLastModified || reportLastVisibleActionLastModified < mostRecentReportActionLastModified) { return; @@ -906,7 +903,7 @@ function isTaskAction(reportAction: OnyxEntry): boolean { */ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined { // If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report. - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { return; } @@ -1145,7 +1142,6 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) { CONST.REPORT.ACTIONS.TYPE.SHARE, CONST.REPORT.ACTIONS.TYPE.STRIPE_PAID, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, - CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, CONST.REPORT.ACTIONS.TYPE.UNSHARE, CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT, CONST.REPORT.ACTIONS.TYPE.DONATION, @@ -1180,7 +1176,7 @@ function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldD case CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD: { const {oldValue, newValue, fieldName} = originalMessage; if (!oldValue) { - Localize.translateLocal('report.actions.type.changeFieldEmpty', {newValue, fieldName}); + return Localize.translateLocal('report.actions.type.changeFieldEmpty', {newValue, fieldName}); } return Localize.translateLocal('report.actions.type.changeField', {oldValue, newValue, fieldName}); } @@ -1337,8 +1333,7 @@ function isActionableJoinRequest(reportAction: OnyxEntry): reportA * @param reportID */ function isActionableJoinRequestPending(reportID: string): boolean { - const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(reportID))); - const findPendingRequest = sortedReportActions.find( + const findPendingRequest = Object.values(getAllReportActions(reportID)).find( (reportActionItem) => isActionableJoinRequest(reportActionItem) && getOriginalMessage(reportActionItem)?.choice === ('' as JoinWorkspaceResolution), ); return !!findPendingRequest; @@ -1384,7 +1379,7 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const reportActions = getAllReportActions(report?.reportID ?? ''); const action = Object.values(reportActions ?? {})?.find((reportAction) => { const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : -1; diff --git a/src/libs/ReportConnection.ts b/src/libs/ReportConnection.ts new file mode 100644 index 000000000000..86e73229e84b --- /dev/null +++ b/src/libs/ReportConnection.ts @@ -0,0 +1,47 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import * as PriorityModeActions from './actions/PriorityMode'; +import * as ReportHelperActions from './actions/Report'; + +// Dynamic Import to avoid circular dependency +const UnreadIndicatorUpdaterHelper = () => import('./UnreadIndicatorUpdater'); + +const reportIDToNameMap: Record = {}; +let allReports: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (value) => { + allReports = value; + UnreadIndicatorUpdaterHelper().then((module) => { + module.triggerUnreadUpdate(); + }); + // Each time a new report is added we will check to see if the user should be switched + PriorityModeActions.autoSwitchToFocusMode(); + + if (!value) { + return; + } + Object.values(value).forEach((report) => { + if (!report) { + return; + } + reportIDToNameMap[report.reportID] = report.reportName ?? report.displayName ?? report.reportID; + ReportHelperActions.handleReportChanged(report); + }); + }, +}); + +// This function is used to get all reports +function getAllReports() { + return allReports; +} + +// This function is used to get all reports name map +function getAllReportsNameMap() { + return reportIDToNameMap; +} + +export {getAllReports, getAllReportsNameMap}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 342e2439ed66..ee8e221e5aba 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -70,6 +70,7 @@ import * as PhoneNumber from './PhoneNumber'; import * as PolicyUtils from './PolicyUtils'; import type {LastVisibleMessage} from './ReportActionsUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; +import * as ReportConnection from './ReportConnection'; import StringUtils from './StringUtils'; import * as SubscriptionUtils from './SubscriptionUtils'; import * as TransactionUtils from './TransactionUtils'; @@ -187,6 +188,11 @@ type OptimisticApprovedReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' >; +type OptimisticUnapprovedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; + type OptimisticSubmittedReportAction = Pick< ReportAction, | 'actionName' @@ -473,7 +479,6 @@ let isAnonymousUser = false; const parsedReportActionMessageCache: Record = {}; const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon'; - Onyx.connect({ key: ONYXKEYS.SESSION, callback: (value) => { @@ -501,13 +506,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - let allReportsDraft: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_DRAFT, @@ -552,23 +550,6 @@ Onyx.connect({ }, }); -let lastUpdatedReport: OnyxEntry; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (value) => { - if (!value) { - return; - } - - lastUpdatedReport = value; - }, -}); - -function getLastUpdatedReport(): OnyxEntry { - return lastUpdatedReport; -} - function getCurrentUserAvatar(): AvatarSource | undefined { return currentUserPersonalDetails?.avatar; } @@ -585,6 +566,7 @@ function getChatType(report: OnyxInputOrEntry | Participant): ValueOf { + const allReports = ReportConnection.getAllReports(); if (!allReports && !allReportsDraft) { return undefined; } @@ -611,7 +593,7 @@ function getParentReport(report: OnyxEntry): OnyxEntry { if (!report?.parentReportID) { return undefined; } - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; + return ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; } /** @@ -652,17 +634,18 @@ function getPolicyType(report: OnyxInputOrEntry, policies: OnyxCollectio return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.type ?? ''; } +const unavailableTranslation = Localize.translateLocal('workspace.common.unavailable'); /** * Get the policy name from a given report */ function getPolicyName(report: OnyxInputOrEntry, returnEmptyIfNotFound = false, policy?: OnyxInputOrEntry): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); + const noPolicyFound = returnEmptyIfNotFound ? '' : unavailableTranslation; if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return Localize.translateLocal('workspace.common.unavailable'); + return unavailableTranslation; } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -706,7 +689,7 @@ function isExpenseReport(report: OnyxInputOrEntry): boolean { * Checks if a report is an IOU report using report or reportID */ function isIOUReport(reportOrID: OnyxInputOrEntry | string): boolean { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return report?.type === CONST.REPORT.TYPE.IOU; } @@ -771,13 +754,20 @@ function isReportManager(report: OnyxEntry): boolean { * Checks if the supplied report has been approved */ function isReportApproved(reportOrID: OnyxInputOrEntry | string, parentReportAction: OnyxEntry = undefined): boolean { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; if (!report) { return parentReportAction?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && parentReportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; } return report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED; } +/** + * Checks if the supplied report has been manually reimbursed + */ +function isReportManuallyReimbursed(report: OnyxEntry): boolean { + return report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; +} + /** * Checks if the supplied report is an expense report in Open state and status. */ @@ -808,6 +798,7 @@ function hasParticipantInArray(report: OnyxEntry, memberAccountIDs: numb * Whether the Money Request report is settled */ function isSettled(reportID: string | undefined): boolean { + const allReports = ReportConnection.getAllReports(); if (!allReports || !reportID) { return false; } @@ -829,6 +820,7 @@ function isSettled(reportID: string | undefined): boolean { * Whether the current user is the submitter of the report */ function isCurrentUserSubmitter(reportID: string): boolean { + const allReports = ReportConnection.getAllReports(); if (!allReports) { return false; } @@ -891,7 +883,7 @@ function isInvoiceRoom(report: OnyxEntry): boolean { function isInvoiceRoomWithID(reportID?: string): boolean { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`]; return isInvoiceRoom(report); } @@ -1011,7 +1003,7 @@ function isWorkspaceTaskReport(report: OnyxEntry): boolean { if (!isTaskReport(report)) { return false; } - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isPolicyExpenseChat(parentReport); } @@ -1052,13 +1044,12 @@ function isSystemChat(report: OnyxEntry): boolean { * Only returns true if this is our main 1:1 DM report with Concierge. */ function isConciergeChatReport(report: OnyxInputOrEntry): boolean { - const participantAccountIDs = Object.keys(report?.participants ?? {}) - .map(Number) - .filter((accountID) => accountID !== currentUserAccountID); - return participantAccountIDs.length === 1 && participantAccountIDs[0] === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); + const participantAccountIDs = Object.keys(report?.participants ?? {}); + return participantAccountIDs.length === 1 && Number(participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } function findSelfDMReportID(): string | undefined { + const allReports = ReportConnection.getAllReports(); if (!allReports) { return; } @@ -1266,7 +1257,7 @@ function isArchivedRoom(report: OnyxInputOrEntry, reportNameValuePairs?: */ function isArchivedRoomWithID(reportID?: string) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`]; return isArchivedRoom(report); } @@ -1398,7 +1389,7 @@ function isChildReport(report: OnyxEntry): boolean { function isExpenseRequest(report: OnyxInputOrEntry): boolean { if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isExpenseReport(parentReport) && !isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; @@ -1411,7 +1402,7 @@ function isExpenseRequest(report: OnyxInputOrEntry): boolean { function isIOURequest(report: OnyxInputOrEntry): boolean { if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; return isIOUReport(parentReport) && !isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; @@ -1433,7 +1424,7 @@ function isTrackExpenseReport(report: OnyxInputOrEntry): boolean { * Checks if a report is an IOU or expense request. */ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOURequest(report) || isExpenseRequest(report); } @@ -1441,7 +1432,7 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { * Checks if a report is an IOU or expense report. */ function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | string): boolean { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOUReport(report) || isExpenseReport(report); } @@ -1668,7 +1659,7 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc // In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will // get parent report and use its participants array. if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) { - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; if (isOneOnOneChat(parentReport)) { finalReport = parentReport; } @@ -1857,6 +1848,7 @@ function getPersonalDetailsForAccountID(accountID: number): Partial | string, shouldUseShortDisplayName = true, ): string { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, shouldUseShortDisplayName) ?? ''; const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction); let messageKey: TranslationPaths; @@ -2238,7 +2230,7 @@ function getReimbursementDeQueuedActionMessage( reportOrID: OnyxEntry | string, isLHNPreview = false, ): string { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction); const amount = originalMessage?.amount; const currency = originalMessage?.currency; @@ -2392,7 +2384,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea } function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allReportsDict?: OnyxCollection): SpendBreakdown { - const allAvailableReports = allReportsDict ?? allReports; + const allAvailableReports = allReportsDict ?? ReportConnection.getAllReports(); let moneyRequestReport; if (isMoneyRequestReport(report) || isInvoiceReport(report)) { moneyRequestReport = report; @@ -2739,7 +2731,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxInputOrEntry } const iouMessage = ReportActionsUtils.getOriginalMessage(reportAction); - const moneyRequestReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouMessage?.IOUReportID}`] ?? ({} as Report); + const moneyRequestReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${iouMessage?.IOUReportID}`] ?? ({} as Report); const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${iouMessage?.IOUTransactionID}`] ?? ({} as Transaction); if (isSettled(String(moneyRequestReport.reportID)) || isReportApproved(String(moneyRequestReport.reportID))) { @@ -2987,7 +2979,7 @@ function getReportPreviewMessage( isForListPreview = false, originalReportAction: OnyxInputOrEntry = iouReportAction, ): string { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; const reportActionMessage = ReportActionsUtils.getReportActionHtml(iouReportAction); if (isEmptyObject(report) || !report?.reportID) { @@ -3455,10 +3447,13 @@ function getReportName(report: OnyxEntry, policy?: OnyxEntry, pa } // Not a room or PolicyExpenseChat, generate title from first 5 other participants - const participantsWithoutCurrentUser = Object.keys(report?.participants ?? {}) - .map(Number) - .filter((accountID) => accountID !== currentUserAccountID) - .slice(0, 5); + const participantsWithoutCurrentUser: number[] = []; + Object.keys(report?.participants ?? {}).forEach((accountID) => { + const accID = Number(accountID); + if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) { + participantsWithoutCurrentUser.push(accID); + } + }); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); } @@ -3959,7 +3954,7 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string, reimbursable = true): OptimisticExpenseReport { // The amount for Expense reports are stored as negative value in the database const storedTotal = total * -1; - const policyName = getPolicyName(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); + const policyName = getPolicyName(ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency); const policy = getPolicy(policyID); @@ -4039,6 +4034,9 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num case CONST.REPORT.ACTIONS.TYPE.APPROVED: iouMessage = `approved ${amount}`; break; + case CONST.REPORT.ACTIONS.TYPE.UNAPPROVED: + iouMessage = `unapproved ${amount}`; + break; case CONST.IOU.REPORT_ACTION_TYPE.CREATE: iouMessage = `submitted ${amount}${comment && ` for ${comment}`}`; break; @@ -4201,6 +4199,36 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e }; } +/** + * Builds an optimistic APPROVED report action with a randomly generated reportActionID. + */ +function buildOptimisticUnapprovedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticUnapprovedReportAction { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, + actorAccountID: currentUserAccountID, + automatic: false, + avatar: getCurrentUserAvatar(), + isAttachment: false, + originalMessage: { + amount, + currency, + expenseReportID, + }, + message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, Math.abs(amount), '', currency), + person: [ + { + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + type: 'TEXT', + }, + ], + reportActionID: NumberUtils.rand64(), + shouldShow: true, + created: DateUtils.getDBTime(), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + /** * Builds an optimistic MOVED report action with a randomly generated reportActionID. * This action is used when we move reports across workspaces. @@ -5219,7 +5247,7 @@ function isUnread(report: OnyxEntry): boolean { } function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict?: OnyxCollection): boolean { - const allAvailableReports = allReportsDict ?? allReports; + const allAvailableReports = allReportsDict ?? ReportConnection.getAllReports(); if (!report || !allAvailableReports) { return false; } @@ -5382,15 +5410,13 @@ function shouldReportBeInOptionList({ // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. // Optionally exclude reports that do not belong to currently active workspace - const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); - if ( !report?.reportID || !report?.type || report?.reportName === undefined || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing report?.isHidden || - (participantAccountIDs.length === 0 && + (!report?.participants && !isChatThread(report) && !isPublicRoom(report) && !isUserCreatedPolicyRoom(report) && @@ -5405,7 +5431,7 @@ function shouldReportBeInOptionList({ return false; } - if (participantAccountIDs.includes(CONST.ACCOUNT_ID.NOTIFICATIONS) && (!currentUserAccountID || !AccountUtils.isAccountIDOddNumber(currentUserAccountID))) { + if (report?.participants?.[CONST.ACCOUNT_ID.NOTIFICATIONS] && (!currentUserAccountID || !AccountUtils.isAccountIDOddNumber(currentUserAccountID))) { return false; } @@ -5501,6 +5527,7 @@ function shouldReportBeInOptionList({ * Returns the system report from the list of reports. */ function getSystemChat(): OnyxEntry { + const allReports = ReportConnection.getAllReports(); if (!allReports) { return undefined; } @@ -5511,7 +5538,7 @@ function getSystemChat(): OnyxEntry { /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, expense, room, and policy expense chat. */ -function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports, shouldIncludeGroupChats = false): OnyxEntry { +function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = ReportConnection.getAllReports(), shouldIncludeGroupChats = false): OnyxEntry { const sortedNewParticipantList = newParticipantList.sort(); return Object.values(reports ?? {}).find((report) => { const participantAccountIDs = Object.keys(report?.participants ?? {}); @@ -5539,7 +5566,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec /** * Attempts to find an invoice chat report in onyx with the provided policyID and receiverID. */ -function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = allReports): OnyxEntry { +function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = ReportConnection.getAllReports()): OnyxEntry { return Object.values(reports ?? {}).find((report) => { if (!report || !isInvoiceRoom(report)) { return false; @@ -5558,7 +5585,7 @@ function getInvoiceChatByParticipants(policyID: string, receiverID: string | num * Attempts to find a policy expense report in onyx that is owned by ownerAccountID in a given policy */ function getPolicyExpenseChat(ownerAccountID: number, policyID: string): OnyxEntry { - return Object.values(allReports ?? {}).find((report: OnyxEntry) => { + return Object.values(ReportConnection.getAllReports() ?? {}).find((report: OnyxEntry) => { // If the report has been deleted, then skip it if (!report) { return false; @@ -5569,7 +5596,7 @@ function getPolicyExpenseChat(ownerAccountID: number, policyID: string): OnyxEnt } function getAllPolicyReports(policyID: string): Array> { - return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID); + return Object.values(ReportConnection.getAllReports() ?? {}).filter((report) => report?.policyID === policyID); } /** @@ -5582,7 +5609,7 @@ function chatIncludesChronos(report: OnyxInputOrEntry): boolean { function chatIncludesChronosWithID(reportID?: string): boolean { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`]; return chatIncludesChronos(report); } @@ -5732,7 +5759,7 @@ function getReportIDFromLink(url: string | null): string { */ function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxInputOrEntry): boolean { if (chatReport?.iouReportID) { - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`]; + const iouReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`]; if (iouReport?.isWaitingOnBankAccount && iouReport?.ownerAccountID === currentUserAccountID) { return true; } @@ -6053,6 +6080,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { * Return true if reports data exists */ function isReportDataReady(): boolean { + const allReports = ReportConnection.getAllReports(); return !isEmptyObject(allReports) && Object.keys(allReports ?? {}).some((key) => allReports?.[key]?.reportID); } @@ -6076,7 +6104,7 @@ function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Error * Return true if the expense report is marked for deletion. */ function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; + const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; if (!isMoneyRequestReport(report)) { return false; } @@ -6104,7 +6132,9 @@ function getOriginalReportID(reportID: string, reportAction: OnyxInputOrEntry, policy: OnyxEntry, } function getWorkspaceChats(policyID: string, accountIDs: number[]): Array> { + const allReports = ReportConnection.getAllReports(); return Object.values(allReports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '-1') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1)); } @@ -6147,6 +6178,7 @@ function getWorkspaceChats(policyID: string, accountIDs: number[]): Array> { + const allReports = ReportConnection.getAllReports(); return Object.values(allReports ?? {}).filter((report) => (report?.policyID ?? '-1') === policyID); } @@ -6451,7 +6483,7 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { } function getRoom(type: ValueOf, policyID: string): OnyxEntry { - const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); + const room = Object.values(ReportConnection.getAllReports() ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } @@ -6817,7 +6849,7 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxInputOrEntry report && report?.[reportFieldToCompare] === tripRoomReportID) .map((report) => report?.reportID); return tripTransactionReportIDs.flatMap((reportID) => TransactionUtils.getAllReportTransactions(reportID)); @@ -7013,6 +7045,7 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: s } function shouldShowMerchantColumn(transactions: Transaction[]) { + const allReports = ReportConnection.getAllReports(); return transactions.some((transaction) => isExpenseReport(allReports?.[transaction.reportID] ?? null)); } @@ -7027,11 +7060,11 @@ function isChatUsedForOnboarding(report: OnyxEntry): boolean { * Get the report (system or concierge chat) used for the user's onboarding process. */ function getChatUsedForOnboarding(): OnyxEntry { - return Object.values(allReports ?? {}).find(isChatUsedForOnboarding); + return Object.values(ReportConnection.getAllReports() ?? {}).find(isChatUsedForOnboarding); } function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry { - return Object.values(allReports ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID); + return Object.values(ReportConnection.getAllReports() ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID); } export { @@ -7039,6 +7072,7 @@ export { areAllRequestsBeingSmartScanned, buildOptimisticAddCommentReportAction, buildOptimisticApprovedReportAction, + buildOptimisticUnapprovedReportAction, buildOptimisticCancelPaymentReportAction, buildOptimisticChangedTaskAssigneeReportAction, buildOptimisticChatReport, @@ -7127,7 +7161,6 @@ export { getIcons, getIconsForParticipants, getIndicatedMissingPaymentMethod, - getLastUpdatedReport, getLastVisibleMessage, getMoneyRequestOptions, getMoneyRequestSpendBreakdown, @@ -7251,6 +7284,7 @@ export { isPublicAnnounceRoom, isPublicRoom, isReportApproved, + isReportManuallyReimbursed, isReportDataReady, isReportFieldDisabled, isReportFieldOfTypeTitle, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 3f7ee1b167c2..4f227e04482a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,7 +1,7 @@ import {Str} from 'expensify-common'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ChatReportSelector, PolicySelector, ReportActionsSelector} from '@hooks/useReportIDs'; +import type {PolicySelector, ReportActionsSelector} from '@hooks/useReportIDs'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx'; @@ -68,7 +68,7 @@ type MiniReport = { */ function getOrderedReportIDs( currentReportId: string | null, - allReports: OnyxCollection, + allReports: OnyxCollection, betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, @@ -82,7 +82,7 @@ function getOrderedReportIDs( const allReportsDictValues = Object.values(allReports ?? {}); // Filter out all the reports that shouldn't be displayed - let reportsToDisplay: Array = []; + let reportsToDisplay: Array = []; allReportsDictValues.forEach((report) => { if (!report) { return; diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index 332d82915463..bd0bd10cd83e 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -1,21 +1,11 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import * as Localize from './Localize'; import {getReportActionHtml, getReportActionText} from './ReportActionsUtils'; - -let allReports: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - allReports = reports; - }, -}); +import * as ReportConnection from './ReportConnection'; /** * Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard. @@ -39,7 +29,7 @@ function getTaskReportActionMessage(action: OnyxEntry): Pick = {}; Onyx.connect({ @@ -37,13 +38,6 @@ Onyx.connect({ callback: (value) => (allTransactionViolations = value), }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - let currentUserEmail = ''; let currentUserAccountID = -1; Onyx.connect({ @@ -197,6 +191,7 @@ function isCreatedMissing(transaction: OnyxEntry) { } function areRequiredFieldsEmpty(transaction: OnyxEntry): boolean { + const allReports = ReportConnection.getAllReports(); const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] ?? null; const isFromExpenseReport = parentReport?.type === CONST.REPORT.TYPE.EXPENSE; const isSplitPolicyExpenseChat = !!transaction?.comment?.splits?.some((participant) => allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`]?.isOwnPolicyExpenseChat); @@ -908,7 +903,7 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia } function getTransactionID(threadReportID: string): string { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`] ?? null; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`] ?? null; const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 7698433c33c1..2546225bd6ea 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -1,17 +1,14 @@ import debounce from 'lodash/debounce'; import memoize from 'lodash/memoize'; import type {OnyxCollection} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; import updateUnread from './updateUnread'; -let allReports: OnyxCollection = {}; - -export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, currentReportID: string) { +function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, currentReportID: string) { return Object.values(reports ?? {}).filter( (report) => ReportUtils.isUnread(report) && @@ -40,23 +37,16 @@ export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollecti const memoizedGetUnreadReportsForUnreadIndicator = memoize(getUnreadReportsForUnreadIndicator); const triggerUnreadUpdate = debounce(() => { - const currentReportID = navigationRef.isReady() ? Navigation.getTopmostReportId() ?? '-1' : '-1'; + const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() ?? '-1' : '-1'; // We want to keep notification count consistent with what can be accessed from the LHN list - const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(allReports, currentReportID); + const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(ReportConnection.getAllReports(), currentReportID); updateUnread(unreadReports.length); }, CONST.TIMING.UNREAD_UPDATE_DEBOUNCE_TIME); -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reportsFromOnyx) => { - allReports = reportsFromOnyx; - triggerUnreadUpdate(); - }, -}); - -navigationRef.addListener('state', () => { +navigationRef?.addListener?.('state', () => { triggerUnreadUpdate(); }); + +export {triggerUnreadUpdate, getUnreadReportsForUnreadIndicator}; diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index 62c034145d4b..d827a6cae000 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -10,20 +10,13 @@ import * as CurrencyUtils from './CurrencyUtils'; import type {Phrase, PhraseParameters} from './Localize'; import * as OptionsListUtils from './OptionsListUtils'; import {hasCustomUnitsError, hasEmployeeListError, hasPolicyError, hasTaxRateError} from './PolicyUtils'; +import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; type CheckingMethod = () => boolean; -let allReports: OnyxCollection; - type BrickRoad = ValueOf | undefined; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - let allPolicies: OnyxCollection; Onyx.connect({ @@ -100,6 +93,7 @@ function hasWorkspaceSettingsRBR(policy: Policy) { } function getChatTabBrickRoad(policyID?: string): BrickRoad | undefined { + const allReports = ReportConnection.getAllReports(); if (!allReports) { return undefined; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 48c70021cacc..ade0a3b56ac8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -24,6 +24,7 @@ import type { StartSplitBillParams, SubmitReportParams, TrackExpenseParams, + UnapproveExpenseReportParams, UpdateMoneyRequestParams, } from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -42,6 +43,7 @@ import Permissions from '@libs/Permissions'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; @@ -165,13 +167,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection | null = null; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value ?? null), -}); - let allReportsDraft: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_DRAFT, @@ -295,6 +290,7 @@ Onyx.connect({ * Get the report or draft report given a reportID */ function getReportOrDraftReport(reportID: string | undefined): OnyxEntry { + const allReports = ReportConnection.getAllReports(); if (!allReports && !allReportsDraft) { return undefined; } @@ -502,7 +498,7 @@ function buildOnyxDataForMoneyRequest( if (TransactionUtils.isDistanceRequest(transaction)) { newQuickAction = CONST.QUICK_ACTIONS.REQUEST_DISTANCE; } - const existingTransactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null; + const existingTransactionThreadReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null; if (chatReport) { optimisticData.push({ @@ -1218,7 +1214,7 @@ function buildOnyxDataForTrackExpense( } else if (isDistanceRequest) { newQuickAction = CONST.QUICK_ACTIONS.TRACK_DISTANCE; } - const existingTransactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null; + const existingTransactionThreadReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null; if (chatReport) { optimisticData.push( @@ -1573,6 +1569,7 @@ function getDeleteTrackExpenseInformation( actionableWhisperReportActionID = '', resolution = '', ) { + const allReports = ReportConnection.getAllReports(); // STEP 1: Get all collections we're updating const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -1916,6 +1913,7 @@ function getMoneyRequestInformation( let isNewChatReport = false; let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; + const allReports = ReportConnection.getAllReports(); // If this is a policyExpenseChat, the chatReport must exist and we can get it from Onyx. // report is null if the flow is initiated from the global create menu. However, participant always stores the reportID if it exists, which is the case for policyExpenseChats if (!chatReport && isPolicyExpenseChat) { @@ -2132,7 +2130,7 @@ function getTrackExpenseInformation( // STEP 1: Get existing chat report let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; - + const allReports = ReportConnection.getAllReports(); // The chatReport always exists, and we can get it from Onyx if chatReport is null. if (!chatReport) { chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null; @@ -2491,6 +2489,7 @@ function getUpdateMoneyRequestParams( const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null])); const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); + const allReports = ReportConnection.getAllReports(); // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -2779,6 +2778,7 @@ function getUpdateTrackExpenseParams( const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null])); const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); + const allReports = ReportConnection.getAllReports(); // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -2945,6 +2945,7 @@ function updateMoneyRequestDate( const transactionChanges: TransactionChanges = { created: value, }; + const allReports = ReportConnection.getAllReports(); const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; let data: UpdateMoneyRequestData; @@ -2985,6 +2986,7 @@ function updateMoneyRequestMerchant( const transactionChanges: TransactionChanges = { merchant: value, }; + const allReports = ReportConnection.getAllReports(); const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; let data: UpdateMoneyRequestData; @@ -3073,6 +3075,7 @@ function updateMoneyRequestDistance({ waypoints, routes, }; + const allReports = ReportConnection.getAllReports(); const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; let data: UpdateMoneyRequestData; @@ -3113,6 +3116,7 @@ function updateMoneyRequestDescription( const transactionChanges: TransactionChanges = { comment, }; + const allReports = ReportConnection.getAllReports(); const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; let data: UpdateMoneyRequestData; @@ -3781,7 +3785,7 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, const existingChatReportID = existingSplitChatReportID || participants[0].reportID; // Check if the report is available locally if we do have one - let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; + let existingSplitChatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; if (!existingSplitChatReport) { @@ -4073,7 +4077,9 @@ function createSplitsAndOnyxData( } // STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one - let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null; + let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID + ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] + : null; const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport); if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) { @@ -4805,6 +4811,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA let oneOnOneChatReport: OnyxEntry; let isNewOneOnOneChatReport = false; + const allReports = ReportConnection.getAllReports(); if (isPolicyExpenseChat) { // The workspace chat reportID is saved in the splits array when starting a split expense with a workspace oneOnOneChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`]; @@ -4962,6 +4969,7 @@ function editRegularMoneyRequest( policyTags: OnyxTypes.PolicyTagList, policyCategories: OnyxTypes.PolicyCategories, ) { + const allReports = ReportConnection.getAllReports(); // STEP 1: Get all collections we're updating const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -5262,6 +5270,7 @@ function updateMoneyRequestAmountAndCurrency({ taxCode, taxAmount, }; + const allReports = ReportConnection.getAllReports(); const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; let data: UpdateMoneyRequestData; @@ -5275,6 +5284,7 @@ function updateMoneyRequestAmountAndCurrency({ } function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { + const allReports = ReportConnection.getAllReports(); // STEP 1: Get all collections we're updating const iouReportID = ReportActionsUtils.isMoneyRequestAction(reportAction) ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID : '-1'; const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; @@ -5586,7 +5596,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor function deleteTrackExpense(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { // STEP 1: Get all collections we're updating - const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; + const chatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; if (!ReportUtils.isSelfDM(chatReport)) { return deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView); } @@ -6359,6 +6369,95 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); } +function unapproveExpenseReport(expenseReport: OnyxEntry) { + if (isEmptyObject(expenseReport)) { + return; + } + + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; + + const optimisticUnapprovedReportAction = ReportUtils.buildOptimisticUnapprovedReportAction(expenseReport.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED); + + const optimisticReportActionData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticUnapprovedReportAction.reportActionID]: { + ...(optimisticUnapprovedReportAction as OnyxTypes.ReportAction), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }; + const optimisticIOUReportData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + ...expenseReport, + lastMessageText: ReportActionsUtils.getReportActionText(optimisticUnapprovedReportAction), + lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticUnapprovedReportAction), + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + pendingFields: { + partial: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }; + + const optimisticNextStepData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }; + + const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionData, optimisticNextStepData]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticUnapprovedReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + pendingFields: { + partial: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticUnapprovedReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: currentNextStep, + }, + ]; + + const parameters: UnapproveExpenseReportParams = { + reportID: expenseReport.reportID, + reportActionID: optimisticUnapprovedReportAction.reportActionID, + }; + + API.write(WRITE_COMMANDS.UNAPPROVE_EXPENSE_REPORT, parameters, {optimisticData, successData, failureData}); +} + function submitReport(expenseReport: OnyxTypes.Report) { if (expenseReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(expenseReport.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID)); @@ -7016,6 +7115,7 @@ function getIOURequestPolicyID(transaction: OnyxEntry, re export { approveMoneyRequest, + unapproveExpenseReport, canApproveIOU, canIOUBePaid, cancelPayment, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index acd42b6202c7..33d906652af6 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -51,6 +51,7 @@ import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; @@ -125,13 +126,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - let lastAccessedWorkspacePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, @@ -259,7 +253,7 @@ function deleteWorkspace(policyID: string, policyName: string) { : []), ]; - const reportsToArchive = Object.values(allReports ?? {}).filter( + const reportsToArchive = Object.values(ReportConnection.getAllReports() ?? {}).filter( (report) => report?.policyID === policyID && (ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report)), ); const finallyData: OnyxUpdate[] = []; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index dfb41b2d9015..2558969be2f3 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -363,7 +363,7 @@ function clearPolicyTagErrors(policyID: string, tagName: string, tagListIndex: n }); } -function clearPolicyTagListError(policyID: string, tagListIndex: number, errorField: string) { +function clearPolicyTagListErrorField(policyID: string, tagListIndex: number, errorField: string) { const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; if (!policyTag.name) { @@ -379,6 +379,20 @@ function clearPolicyTagListError(policyID: string, tagListIndex: number, errorFi }); } +function clearPolicyTagListErrors(policyID: string, tagListIndex: number) { + const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; + + if (!policyTag.name) { + return; + } + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, { + [policyTag.name]: { + errors: null, + }, + }); +} + function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: string}, tagListIndex: number) { const tagList = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; const tag = tagList.tags?.[policyTag.oldName]; @@ -569,7 +583,7 @@ function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: stri onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - [newName]: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + [newName]: {...oldPolicyTags, name: newName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, errors: null}, [oldName]: null, }, }, @@ -589,12 +603,12 @@ function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: stri onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, value: { - errors: { - [oldName]: oldName, - [newName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), - }, [newName]: null, - [oldName]: oldPolicyTags, + [oldName]: { + ...oldPolicyTags, + pendingAction: null, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), + }, }, }, ], @@ -725,7 +739,8 @@ export { setPolicyTagsRequired, createPolicyTag, clearPolicyTagErrors, - clearPolicyTagListError, + clearPolicyTagListErrors, + clearPolicyTagListErrorField, deletePolicyTags, enablePolicyTags, openPolicyTagsPage, diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index a4561d44d5a0..beec327a2e40 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -1,11 +1,10 @@ import debounce from 'lodash/debounce'; -import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; /** * This actions file is used to automatically switch a user into #focus mode when they exceed a certain number of reports. We do this primarily for performance reasons. @@ -35,18 +34,6 @@ Onyx.connect({ // eslint-disable-next-line @typescript-eslint/no-use-before-define const autoSwitchToFocusMode = debounce(tryFocusModeUpdate, 300, {leading: true}); -let allReports: OnyxCollection | undefined = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - allReports = reports; - - // Each time a new report is added we will check to see if the user should be switched - autoSwitchToFocusMode(); - }, -}); - let isLoadingReportData = true; Onyx.connect({ key: ONYXKEYS.IS_LOADING_REPORT_DATA, @@ -87,11 +74,10 @@ function resetHasReadRequiredDataFromStorage() { resolveIsReadyPromise = resolve; }); isLoadingReportData = true; - allReports = {}; } function checkRequiredData() { - if (allReports === undefined || hasTriedFocusMode === undefined || isInFocusMode === undefined || isLoadingReportData) { + if (ReportConnection.getAllReports() === undefined || hasTriedFocusMode === undefined || isInFocusMode === undefined || isLoadingReportData) { return; } @@ -112,6 +98,7 @@ function tryFocusModeUpdate() { } const validReports = []; + const allReports = ReportConnection.getAllReports(); Object.keys(allReports ?? {}).forEach((key) => { const report = allReports?.[key]; if (!report) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a1a52b64d49f..4d674726540a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -70,6 +70,7 @@ import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; @@ -190,20 +191,6 @@ Onyx.connect({ }, }); -const currentReportData: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (report, key) => { - if (!key || !report) { - return; - } - const reportID = CollectionUtils.extractCollectionItemID(key); - currentReportData[reportID] = report; - // eslint-disable-next-line @typescript-eslint/no-use-before-define - handleReportChanged(report); - }, -}); - let isNetworkOffline = false; let networkStatus: NetworkStatus; Onyx.connect({ @@ -242,7 +229,6 @@ Onyx.connect({ callback: (value) => (reportMetadata = value), }); -const allReports: OnyxCollection = {}; const typingWatchTimers: Record = {}; let reportIDDeeplinkedFromOldDot: string | undefined; @@ -493,7 +479,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { lastReadTime: currentTime, }; - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (!isEmptyObject(report) && ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { optimisticReport.notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS; @@ -667,7 +653,7 @@ function updateGroupChatName(reportID: string, reportName: string) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportName: currentReportData?.[reportID]?.reportName ?? null, + reportName: ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? null, errors: { reportName: Localize.translateLocal('common.genericErrorMessage'), }, @@ -704,7 +690,7 @@ function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageMani onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - avatarUrl: currentReportData?.[reportID]?.avatarUrl ?? null, + avatarUrl: ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.avatarUrl ?? null, pendingFields: { avatar: null, }, @@ -767,7 +753,7 @@ function openReport( const optimisticReport = reportActionsExist(reportID) ? {} : { - reportName: allReports?.[reportID]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, + reportName: ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, }; const optimisticData: OnyxUpdate[] = [ @@ -951,7 +937,7 @@ function openReport( } } - parameters.clientLastReadTime = currentReportData?.[reportID]?.lastReadTime ?? ''; + parameters.clientLastReadTime = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.lastReadTime ?? ''; const paginationConfig = { resourceID: reportID, @@ -1060,7 +1046,7 @@ function navigateToAndOpenChildReport(childReportID = '-1', parentReportAction: Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); } else { const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])]; - const parentReport = allReports?.[parentReportID]; + const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; // Threads from DMs and selfDMs don't have a chatType. All other threads inherit the chatType from their parent const childReportChatType = parentReport && ReportUtils.isSelfDM(parentReport) ? undefined : parentReport?.chatType; const newChat = ReportUtils.buildOptimisticChatReport( @@ -1252,7 +1238,9 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) { }, null); // If no action created date is provided, use the last action's from other user - const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? allReports?.[reportID]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0)); + const actionCreationTime = + reportActionCreated || + (latestReportActionFromOtherUsers?.created ?? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' @@ -1354,9 +1342,7 @@ function handleReportChanged(report: OnyxEntry) { return; } - if (allReports && report?.reportID) { - allReports[report.reportID] = report; - + if (report?.reportID) { if (ReportUtils.isConciergeChatReport(report)) { conciergeChatReportID = report.reportID; } @@ -1757,7 +1743,7 @@ function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction: } } else { const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction?.actorAccountID)])]; - const parentReport = allReports?.[parentReportID]; + const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; const newChat = ReportUtils.buildOptimisticChatReport( participantAccountIDs, ReportActionsUtils.getReportActionText(parentReportAction), @@ -2170,7 +2156,7 @@ function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) { /** Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. */ function deleteReport(reportID: string) { - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const onyxData: Record = { [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: null, @@ -2322,7 +2308,7 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi } // We don't want to send a local notification if the user preference is daily, mute or hidden. - const notificationPreference = allReports?.[reportID]?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS; + const notificationPreference = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS; if (notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS) { Log.info(`${tag} No notification because user preference is to be notified: ${notificationPreference}`); return false; @@ -2340,7 +2326,7 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi return false; } - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (!report || (report && report.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { Log.info(`${tag} No notification because the report does not exist or is pending deleted`, false); return false; @@ -2374,9 +2360,10 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi Log.info('[LocalNotification] Creating notification'); - const report = allReports?.[reportID] ?? null; + const localReportID = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + const report = ReportConnection.getAllReports()?.[localReportID] ?? null; if (!report) { - Log.hmmm("[LocalNotification] couldn't show report action notification because the report wasn't found", {reportID, reportActionID: reportAction.reportActionID}); + Log.hmmm("[LocalNotification] couldn't show report action notification because the report wasn't found", {localReportID, reportActionID: reportAction.reportActionID}); return; } @@ -2596,7 +2583,17 @@ function getCurrentUserAccountID(): number { } function navigateToMostRecentReport(currentReport: OnyxEntry) { - const lastAccessedReportID = ReportUtils.findLastAccessedReport(allReports, false, undefined, false, false, reportMetadata, undefined, [], currentReport?.reportID)?.reportID; + const lastAccessedReportID = ReportUtils.findLastAccessedReport( + ReportConnection.getAllReports(), + false, + undefined, + false, + false, + reportMetadata, + undefined, + [], + currentReport?.reportID, + )?.reportID; if (lastAccessedReportID) { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1'); @@ -2621,7 +2618,7 @@ function joinRoom(report: OnyxEntry) { } function leaveGroupChat(reportID: string) { - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (!report) { Log.warn('Attempting to leave Group Chat that does not existing locally'); return; @@ -2649,7 +2646,7 @@ function leaveGroupChat(reportID: string) { /** Leave a report by setting the state to submitted and closed */ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = false) { - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (!report) { return; @@ -2732,7 +2729,7 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal /** Invites people to a room */ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmailsToAccountIDs) { - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (!report) { return; } @@ -2826,7 +2823,7 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails } function clearAddRoomMemberError(reportID: string, invitedAccountID: string) { - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { pendingChatMembers: report?.pendingChatMembers?.filter((pendingChatMember) => pendingChatMember.accountID !== invitedAccountID), participants: { @@ -2888,7 +2885,7 @@ function inviteToGroupChat(reportID: string, inviteeEmailsToAccountIDs: InvitedE * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { - const report = currentReportData?.[reportID]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (!report) { return; } @@ -3846,4 +3843,5 @@ export { updateLoadingInitialReportAction, clearAddRoomMemberError, clearAvatarErrors, + handleReportChanged, }; diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 395c99fc4b26..b3718079441f 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -1,10 +1,11 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report as OnyxReportType, ReportActions} from '@src/types/onyx'; +import type {ReportActions} from '@src/types/onyx'; import type ReportAction from '@src/types/onyx/ReportAction'; import * as Report from './Report'; @@ -17,13 +18,6 @@ Onyx.connect({ callback: (value) => (allReportActions = value), }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - function clearReportActionErrors(reportID: string, reportAction: ReportAction, keys?: string[]) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); @@ -85,7 +79,7 @@ function clearAllRelatedReportActionErrors(reportID: string, reportAction: Repor clearReportActionErrors(reportID, reportAction, keys); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.parentReportID && report?.parentReportActionID && ignore !== 'parent') { const parentReportAction = ReportActionUtils.getReportAction(report.parentReportID, report.parentReportActionID); const parentErrorKeys = Object.keys(parentReportAction?.errors ?? {}).filter((err) => errorKeys.includes(err)); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 013ae698ed3f..0a7244bde1e5 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -12,6 +12,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import CONST from '@src/CONST'; @@ -77,13 +78,6 @@ Onyx.connect({ }, }); -let allReports: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (value) => (allReports = value), -}); - /** * Clears out the task info from the store */ @@ -909,14 +903,14 @@ function getParentReport(report: OnyxEntry): OnyxEntry { - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + return ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; } /** @@ -1129,7 +1123,7 @@ function canModifyTask(taskReport: OnyxEntry, sessionAccountID } function clearTaskErrors(reportID: string) { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; // Delete the task preview in the parent report if (report?.pendingFields?.createChat === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index cee4e24041f1..a90c386d02b6 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -1,5 +1,5 @@ import {NativeModules} from 'react-native'; -import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -9,7 +9,6 @@ import type {OnboardingPurposeType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type Onboarding from '@src/types/onyx/Onboarding'; -import type OnyxPolicy from '@src/types/onyx/Policy'; import type TryNewDot from '@src/types/onyx/TryNewDot'; let onboarding: Onboarding | [] | undefined; @@ -202,23 +201,6 @@ Onyx.connect({ }, }); -const allPolicies: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY, - callback: (val, key) => { - if (!key) { - return; - } - - if (val === null || val === undefined) { - delete allPolicies[key]; - return; - } - - allPolicies[key] = {...allPolicies[key], ...val}; - }, -}); - Onyx.connect({ key: ONYXKEYS.NVP_TRYNEWDOT, callback: (value) => { diff --git a/src/libs/markAllPolicyReportsAsRead.ts b/src/libs/markAllPolicyReportsAsRead.ts index 49001a851cf5..259a5e426d89 100644 --- a/src/libs/markAllPolicyReportsAsRead.ts +++ b/src/libs/markAllPolicyReportsAsRead.ts @@ -1,32 +1,21 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; import * as ReportActionFile from './actions/Report'; +import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; export default function markAllPolicyReportsAsRead(policyID: string) { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - if (!allReports) { - return; - } + let delay = 0; + const allReports = ReportConnection.getAllReports() ?? {}; + Object.keys(allReports).forEach((key: string) => { + const report: Report | null | undefined = allReports[key]; + if (report?.policyID !== policyID || !ReportUtils.isUnread(report)) { + return; + } - let delay = 0; - Object.keys(allReports).forEach((key: string) => { - const report: Report | null | undefined = allReports[key]; - if (report?.policyID !== policyID || !ReportUtils.isUnread(report)) { - return; - } + setTimeout(() => { + ReportActionFile.readNewestAction(report?.reportID); + }, delay); - setTimeout(() => { - ReportActionFile.readNewestAction(report?.reportID); - }, delay); - - delay += 1000; - }); - Onyx.disconnect(connectionID); - }, + delay += 1000; }); } diff --git a/src/libs/migrations/Participants.ts b/src/libs/migrations/Participants.ts index 3dbbef486d68..eccaa0662f2f 100644 --- a/src/libs/migrations/Participants.ts +++ b/src/libs/migrations/Participants.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Log from '@libs/Log'; +import * as ReportConnection from '@libs/ReportConnection'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; import type {Participants} from '@src/types/onyx/Report'; @@ -11,14 +12,7 @@ type OldReportCollection = Record>; function getReports(): Promise> { return new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - Onyx.disconnect(connectionID); - return resolve(reports); - }, - }); + resolve(ReportConnection.getAllReports()); }); } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 58a0fe1a80b8..c6e25bcaa70a 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -21,6 +21,7 @@ import PromotedActionsBar, {PromotedActions} from '@components/PromotedActionsBa import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -107,6 +108,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false); const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`], [policies, report?.policyID]); const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]); const isPolicyEmployee = useMemo(() => PolicyUtils.isPolicyEmployee(report?.policyID ?? '-1', policies), [report?.policyID, policies]); @@ -179,7 +181,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; const isDeletedParentAction = ReportActionsUtils.isDeletedAction(requestParentReportAction); - const moneyRequestReport = useMemo(() => { + const moneyRequestReport: OnyxEntry = useMemo(() => { if (caseID === CASES.MONEY_REQUEST) { return parentReport; } @@ -197,6 +199,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; const shouldShowDeleteButton = shouldShowTaskDeleteButton || canDeleteRequest; + const canUnapproveRequest = + ReportUtils.isMoneyRequestReport(moneyRequestReport) && + (ReportUtils.isReportManager(moneyRequestReport) || isPolicyAdmin) && + (ReportUtils.isReportApproved(moneyRequestReport) || ReportUtils.isReportManuallyReimbursed(moneyRequestReport)); + useEffect(() => { if (canDeleteRequest) { return; @@ -224,6 +231,15 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD Report.leaveGroupChat(report.reportID); }, [isChatRoom, isPolicyEmployee, isPolicyExpenseChat, report.reportID, report.visibility]); + const unapproveExpenseReportOrShowModal = useCallback(() => { + if (PolicyUtils.hasAccountingConnections(policy)) { + setIsUnapproveModalVisible(true); + return; + } + Navigation.dismissModal(); + IOU.unapproveExpenseReport(moneyRequestReport); + }, [moneyRequestReport, policy]); + const shouldShowLeaveButton = !isThread && (isGroupChat || (isChatRoom && ReportUtils.canLeaveChat(report, policy)) || (isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !isPolicyAdmin)); @@ -356,6 +372,16 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }, }); } + + if (canUnapproveRequest) { + items.push({ + key: CONST.REPORT_DETAILS_MENU_ITEM.UNAPPROVE, + icon: Expensicons.CircularArrowBackwards, + translationKey: 'iou.unapprove', + isAnonymousAction: false, + action: () => unapproveExpenseReportOrShowModal(), + }); + } return items; }, [ isSelfDM, @@ -380,6 +406,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD isPolicyAdmin, session, leaveChat, + canUnapproveRequest, + unapproveExpenseReportOrShowModal, ]); const displayNamesWithTooltips = useMemo(() => { @@ -399,6 +427,14 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD /> ) : null; + const connectedIntegration = Object.values(CONST.POLICY.CONNECTIONS.NAME).find((integration) => !!policy?.connections?.[integration]); + const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', connectedIntegration) : ''; + const unapproveWarningText = ( + + {translate('iou.headsUp')} {translate('iou.unapproveWithIntegrationWarning', connectedIntegrationName)} + + ); + const renderedAvatar = useMemo(() => { if (isMoneyRequestReport || isInvoiceReport) { return ( @@ -686,6 +722,20 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD danger shouldEnableNewFocusManagement /> + { + setIsUnapproveModalVisible(false); + Navigation.dismissModal(); + IOU.unapproveExpenseReport(moneyRequestReport); + }} + cancelText={translate('common.cancel')} + onCancel={() => setIsUnapproveModalVisible(false)} + prompt={unapproveWarningText} + /> ); diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 27227251b0b6..088ee9eb2b6e 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -69,6 +69,7 @@ const MUTED_ACTIONS = [ ...Object.values(CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG), CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.APPROVED, + CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, CONST.REPORT.ACTIONS.TYPE.MOVED, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, ] as ReportActionName[]; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 635d4df5cb58..471e0d0bf20e 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -2,17 +2,14 @@ import {useIsFocused} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useEffect, useRef} 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 type {EdgeInsets} from 'react-native-safe-area-context'; import type {ValueOf} from 'type-fest'; import useActiveWorkspaceFromNavigationState from '@hooks/useActiveWorkspaceFromNavigationState'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import type {PolicySelector} from '@hooks/useReportIDs'; -import {policySelector, useReportIDs} from '@hooks/useReportIDs'; +import {useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -24,9 +21,6 @@ type SidebarLinksDataOnyxProps = { /** The chat priority mode */ priorityMode: OnyxEntry>; - - /** The policies which the user has access to */ - policies: OnyxCollection; }; type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { @@ -37,21 +31,18 @@ type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { insets: EdgeInsets; }; -function SidebarLinksData({insets, isLoadingApp = true, onLinkClick, priorityMode = CONST.PRIORITY_MODE.DEFAULT, policies}: SidebarLinksDataProps) { - const {accountID} = useCurrentUserPersonalDetails(); +function SidebarLinksData({insets, isLoadingApp = true, onLinkClick, priorityMode = CONST.PRIORITY_MODE.DEFAULT}: SidebarLinksDataProps) { const isFocused = useIsFocused(); const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); - const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); + const {orderedReportIDs, currentReportID, policyMemberAccountIDs} = useReportIDs(); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '-1', policyMemberAccountIDs), [activeWorkspaceID]); const isLoading = isLoadingApp; - const {orderedReportIDs, currentReportID} = useReportIDs(); - const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; const isActiveReport = useCallback((reportID: string): boolean => currentReportIDRef.current === reportID, []); @@ -88,11 +79,6 @@ export default withOnyx({ key: ONYXKEYS.NVP_PRIORITY_MODE, initialValue: CONST.PRIORITY_MODE.DEFAULT, }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - initialValue: {}, - }, })( /* While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs. @@ -105,7 +91,6 @@ More details - https://github.com/Expensify/App/issues/35234#issuecomment-192691 prevProps.isLoadingApp === nextProps.isLoadingApp && prevProps.priorityMode === nextProps.priorityMode && lodashIsEqual(prevProps.insets, nextProps.insets) && - prevProps.onLinkClick === nextProps.onLinkClick && - lodashIsEqual(prevProps.policies, nextProps.policies), + prevProps.onLinkClick === nextProps.onLinkClick, ), ); diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx index 92d814604e57..009b289c9bb4 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceCardListRow.tsx @@ -1,6 +1,5 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; import Avatar from '@components/Avatar'; import Badge from '@components/Badge'; import Text from '@components/Text'; @@ -13,9 +12,6 @@ import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; type WorkspacesListRowProps = { - /** Additional styles applied to the row */ - style: StyleProp; - /** The last four digits of the card */ lastFourPAN: string; @@ -32,14 +28,14 @@ type WorkspacesListRowProps = { currency: string; }; -function WorkspaceCardListRow({style, limit, cardholder, lastFourPAN, name, currency}: WorkspacesListRowProps) { +function WorkspaceCardListRow({limit, cardholder, lastFourPAN, name, currency}: WorkspacesListRowProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const cardholderName = useMemo(() => PersonalDetailsUtils.getDisplayNameOrDefault(cardholder), [cardholder]); return ( - + - {}} // TODO: add navigation action when card details screen is implemented (https://github.com/Expensify/App/issues/44325) > - {({hovered}) => ( - - )} - + + ); diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index 15e9e605e8d4..c479923098c2 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -30,21 +30,22 @@ type WorkspaceTagsSettingsPageOnyxProps = { type WorkspaceTagsSettingsPageProps = WorkspaceTagsSettingsPageOnyxProps & StackScreenProps; function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPageProps) { + const policyID = route.params.policyID; const styles = useThemeStyles(); const {translate} = useLocalize(); const [policyTagLists, isMultiLevelTags] = useMemo(() => [PolicyUtils.getTagLists(policyTags), PolicyUtils.isMultiLevelTags(policyTags)], [policyTags]); const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(Object.values(policyTags ?? {}).flatMap(({tags}) => Object.values(tags))); const updateWorkspaceRequiresTag = useCallback( (value: boolean) => { - Tag.setPolicyRequiresTag(route.params.policyID, value); + Tag.setPolicyRequiresTag(policyID, value); }, - [route.params.policyID], + [policyID], ); return ( {({policy}) => ( @@ -75,13 +76,14 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag {!isMultiLevelTags && ( Tag.clearPolicyTagListErrors(policyID, policyTagLists[0].orderWeight)} pendingAction={policyTags?.[policyTagLists[0].name]?.pendingAction} errorRowStyles={styles.mh5} > Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID, policyTagLists[0].orderWeight))} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(policyID, policyTagLists[0].orderWeight))} shouldShowRightIcon /> diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 07ba76e7c341..5aec0b7c3ca0 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -245,7 +245,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { onToggle={(on) => Tag.setPolicyTagsRequired(policyID, on, route.params.orderWeight)} pendingAction={currentPolicyTag.pendingFields?.required} errors={currentPolicyTag?.errorFields?.required ?? undefined} - onCloseError={() => Tag.clearPolicyTagListError(policyID, route.params.orderWeight, 'required')} + onCloseError={() => Tag.clearPolicyTagListErrorField(policyID, route.params.orderWeight, 'required')} disabled={!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)} /> diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c7d9397f3202..3d864523e418 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -402,6 +402,18 @@ type OriginalMessageApproved = { expenseReportID: string; }; +/** Model of `unapproved` report action */ +type OriginalMessageUnapproved = { + /** Unapproved expense amount */ + amount: number; + + /** Currency of the unapproved expense amount */ + currency: string; + + /** Report ID of the expense */ + expenseReportID: string; +}; + /** The map type of original message */ type OriginalMessageMap = { /** */ @@ -497,7 +509,7 @@ type OriginalMessageMap = { /** */ [CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL]: never; /** */ - [CONST.REPORT.ACTIONS.TYPE.UNAPPROVED]: never; + [CONST.REPORT.ACTIONS.TYPE.UNAPPROVED]: OriginalMessageUnapproved; /** */ [CONST.REPORT.ACTIONS.TYPE.UNHOLD]: never; /** */ diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 24ea7fb4504f..54b3d58785a0 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -262,7 +262,7 @@ describe('actions/Policy', () => { expect(policyTags?.[newTagListName]).toBeFalsy(); expect(policyTags?.[oldTagListName]).toBeTruthy(); - expect(policyTags?.errors).toBeTruthy(); + expect(policyTags?.[oldTagListName]?.errors).toBeTruthy(); resolve(); }, diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 4bbcafa76bbb..4a6b12d726d9 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -2,13 +2,13 @@ import {rand} from '@ngneat/falso'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; -import type {ChatReportSelector} from '@hooks/useReportIDs'; import {getReportActionMessage} from '@libs/ReportActionsUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; +import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; @@ -21,7 +21,7 @@ const REPORTS_COUNT = 15000; const REPORT_TRESHOLD = 5; const PERSONAL_DETAILS_LIST_COUNT = 1000; -const allReports = createCollection( +const allReports = createCollection( (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, (index) => ({ ...createRandomReport(index), diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 0abfa2efd752..41e85c4fdd78 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -873,13 +873,13 @@ describe('Sidebar', () => { return ( waitForBatchedUpdates() + .then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, LHNTestUtils.fakePersonalDetails)) .then(() => LHNTestUtils.getDefaultRenderedSidebarLinks('0')) // Given the sidebar is rendered in #focus mode (hides read chats) // with all reports having unread comments .then(() => Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), diff --git a/tests/unit/UnreadIndicatorUpdaterTest.ts b/tests/unit/UnreadIndicatorUpdaterTest.ts index 22141eee791d..9cf65bcb69d4 100644 --- a/tests/unit/UnreadIndicatorUpdaterTest.ts +++ b/tests/unit/UnreadIndicatorUpdaterTest.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import CONST from '../../src/CONST'; -import getUnreadReportsForUnreadIndicator from '../../src/libs/UnreadIndicatorUpdater'; +import * as UnreadIndicatorUpdater from '../../src/libs/UnreadIndicatorUpdater'; describe('UnreadIndicatorUpdaterTest', () => { describe('should return correct number of unread reports', () => { @@ -24,7 +24,7 @@ describe('UnreadIndicatorUpdaterTest', () => { }, 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastMessageText: 'test'}, }; - expect(getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(2); + expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(2); }); it('given some reports are incomplete', () => { @@ -33,7 +33,7 @@ describe('UnreadIndicatorUpdaterTest', () => { 2: {reportID: '2', type: CONST.REPORT.TYPE.TASK, lastReadTime: '2023-02-05 09:12:05.000', lastVisibleActionCreated: '2023-02-06 07:15:44.030'}, 3: {reportID: '3', type: CONST.REPORT.TYPE.TASK}, }; - expect(getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(0); + expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(0); }); it('given notification preference of some reports is hidden', () => { @@ -57,7 +57,7 @@ describe('UnreadIndicatorUpdaterTest', () => { }, 3: {reportID: '3', reportName: 'test', type: CONST.REPORT.TYPE.TASK, lastMessageText: 'test'}, }; - expect(getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(1); + expect(UnreadIndicatorUpdater.getUnreadReportsForUnreadIndicator(reportsToBeUsed, '3').length).toBe(1); }); }); });