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);
});
});
});