From 3223df7c4bc04a8a04b1a9d9465f049f4957d84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 16 Apr 2026 16:45:05 +0100 Subject: [PATCH 01/13] Use newest Onyx changes --- package-lock.json | 8 ++++---- package.json | 2 +- patches/react-native-onyx/details.md | 2 +- ...e-onyx+3.0.63.patch => react-native-onyx+3.0.67.patch} | 0 4 files changed, 6 insertions(+), 6 deletions(-) rename patches/react-native-onyx/{react-native-onyx+3.0.63.patch => react-native-onyx+3.0.67.patch} (100%) diff --git a/package-lock.json b/package-lock.json index b373f4e340fc..e6c9835c384e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "3.0.63", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#51f93a258b6bc981b8c2d85ab80a76c882be6cf3", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34594,9 +34594,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.63", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.63.tgz", - "integrity": "sha512-6UUpjyUh9viXll9M2YomXM9PMg+791foSPA7E0fazgWfrUAmofNwo4xkXT4k3NDdToZFOYxaXL0uw5z00g/MOA==", + "version": "3.0.67", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#51f93a258b6bc981b8c2d85ab80a76c882be6cf3", + "integrity": "sha512-QWLPKwQ2f46lIxh8IM2hGIy06BSj4eOseSIcmUAJh9GaGPb9epRlwWPYBuqY2HTCKH9YXZYSgBa4x/fk68iVSA==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 6ed1c438f107..17e9c313b208 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "3.0.63", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#51f93a258b6bc981b8c2d85ab80a76c882be6cf3", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", diff --git a/patches/react-native-onyx/details.md b/patches/react-native-onyx/details.md index 67ad7c054e85..03302027420e 100644 --- a/patches/react-native-onyx/details.md +++ b/patches/react-native-onyx/details.md @@ -1,6 +1,6 @@ # `react-native-onyx` patches -### [react-native-onyx+3.0.63.patch](react-native-onyx+3.0.63.patch) +### [react-native-onyx+3.0.67.patch](react-native-onyx+3.0.63.patch) - Reason: Onyx v3.0.59 ([PR #756](https://github.com/Expensify/react-native-onyx/pull/756)) added a state reset inside the `subscribe` callback of `useOnyx` to fix stale data when keys change dynamically. However, this reset runs unconditionally — including on initial mount — which causes `useSyncExternalStore` to see a new snapshot reference after subscription, triggering one extra render per `useOnyx` hook. This patch guards the reset with a `hasMountedRef` flag so it only runs on key-change re-subscriptions, not on initial mount. - E/App issue: https://github.com/Expensify/App/issues/85416 diff --git a/patches/react-native-onyx/react-native-onyx+3.0.63.patch b/patches/react-native-onyx/react-native-onyx+3.0.67.patch similarity index 100% rename from patches/react-native-onyx/react-native-onyx+3.0.63.patch rename to patches/react-native-onyx/react-native-onyx+3.0.67.patch From d4052e20554d6f7cadbfee5b8cfda4167d89ab34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 21 Apr 2026 18:10:29 +0100 Subject: [PATCH 02/13] Use newest Onyx changes --- package-lock.json | 8 ++++---- package.json | 2 +- patches/react-native-onyx/details.md | 2 +- ...e-onyx+3.0.67.patch => react-native-onyx+3.0.69.patch} | 0 4 files changed, 6 insertions(+), 6 deletions(-) rename patches/react-native-onyx/{react-native-onyx+3.0.67.patch => react-native-onyx+3.0.69.patch} (100%) diff --git a/package-lock.json b/package-lock.json index f99d9cb7ca29..290aa29d8448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#51f93a258b6bc981b8c2d85ab80a76c882be6cf3", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#65788d4ff2d6ff97df89b40188585fbef8bf6fe9", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34628,9 +34628,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.67", - "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#51f93a258b6bc981b8c2d85ab80a76c882be6cf3", - "integrity": "sha512-QWLPKwQ2f46lIxh8IM2hGIy06BSj4eOseSIcmUAJh9GaGPb9epRlwWPYBuqY2HTCKH9YXZYSgBa4x/fk68iVSA==", + "version": "3.0.69", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#65788d4ff2d6ff97df89b40188585fbef8bf6fe9", + "integrity": "sha512-ZYhb7TYAROvxK8Lkv001zFtpT7HhpsV5RV6hBo7WCQwvTd6ScxDtDsP3/ctx7s9Dhwzn1stc0qDu8m64JlT8gA==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 4f64ef393894..4f265e12be27 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#51f93a258b6bc981b8c2d85ab80a76c882be6cf3", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#65788d4ff2d6ff97df89b40188585fbef8bf6fe9", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", diff --git a/patches/react-native-onyx/details.md b/patches/react-native-onyx/details.md index 03302027420e..b544adfc56b9 100644 --- a/patches/react-native-onyx/details.md +++ b/patches/react-native-onyx/details.md @@ -1,6 +1,6 @@ # `react-native-onyx` patches -### [react-native-onyx+3.0.67.patch](react-native-onyx+3.0.63.patch) +### [react-native-onyx+3.0.69.patch](react-native-onyx+3.0.69.patch) - Reason: Onyx v3.0.59 ([PR #756](https://github.com/Expensify/react-native-onyx/pull/756)) added a state reset inside the `subscribe` callback of `useOnyx` to fix stale data when keys change dynamically. However, this reset runs unconditionally — including on initial mount — which causes `useSyncExternalStore` to see a new snapshot reference after subscription, triggering one extra render per `useOnyx` hook. This patch guards the reset with a `hasMountedRef` flag so it only runs on key-change re-subscriptions, not on initial mount. - E/App issue: https://github.com/Expensify/App/issues/85416 diff --git a/patches/react-native-onyx/react-native-onyx+3.0.67.patch b/patches/react-native-onyx/react-native-onyx+3.0.69.patch similarity index 100% rename from patches/react-native-onyx/react-native-onyx+3.0.67.patch rename to patches/react-native-onyx/react-native-onyx+3.0.69.patch From 450c8680650b20213e9083d93418458c0f252a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 21 Apr 2026 18:19:30 +0100 Subject: [PATCH 03/13] Fix usage of OnyxUtils.get --- .../GPSTripStateChecker/index.native.tsx | 2 +- src/libs/GPSDraftDetailsUtils.ts | 2 +- src/libs/actions/OnyxDerived/index.ts | 200 +++++++++--------- .../index.native.ts | 4 +- tests/actions/PolicyCategoryTest.ts | 8 +- tests/actions/PolicyTagTest.ts | 64 +++--- tests/actions/ReportTest.ts | 6 +- tests/ui/UnreadIndicatorsTest.tsx | 12 +- .../IOURequestStepConfirmationPageTest.tsx | 14 +- tests/unit/OnyxDerivedTest.tsx | 80 +++---- tests/unit/PersistedRequests.ts | 4 +- tests/unit/TransactionTest.ts | 42 ++-- tests/unit/WorkspacesSettingsUtilsTest.ts | 12 +- tests/unit/canEditFieldOfMoneyRequestTest.ts | 12 +- 14 files changed, 230 insertions(+), 232 deletions(-) diff --git a/src/components/GPSTripStateChecker/index.native.tsx b/src/components/GPSTripStateChecker/index.native.tsx index 3b468dbaa87f..06e9296145f7 100644 --- a/src/components/GPSTripStateChecker/index.native.tsx +++ b/src/components/GPSTripStateChecker/index.native.tsx @@ -33,7 +33,7 @@ function GPSTripStateChecker() { useEffect(() => { async function handleGpsTripInProgressOnAppRestart() { await checkAndCleanGpsNotification(); - const gpsTrip = await OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS); + const gpsTrip = OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS); if (!gpsTrip?.isTracking) { return; diff --git a/src/libs/GPSDraftDetailsUtils.ts b/src/libs/GPSDraftDetailsUtils.ts index 575be1482e08..2f2ffe810f43 100644 --- a/src/libs/GPSDraftDetailsUtils.ts +++ b/src/libs/GPSDraftDetailsUtils.ts @@ -93,7 +93,7 @@ function coordinatesToString(gpsPoint: {lat: number; long: number}): string { } async function getLastPoint() { - const gpsTrip = await OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS); + const gpsTrip = OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS); return gpsTrip?.gpsPoints?.at(-1); } diff --git a/src/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts index 57950c582d5f..b1ad26efb4d0 100644 --- a/src/libs/actions/OnyxDerived/index.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -33,118 +33,116 @@ function init() { // We cast its type to match the tuple expected by config.compute. const dependencyValues = new Array(totalConnections) as Parameters[0]; - OnyxUtils.get(key).then((storedDerivedValue) => { - let derivedValue = storedDerivedValue; - if (derivedValue) { - Log.info(`Derived value for ${key} restored from disk`); - } + let derivedValue = OnyxUtils.get(key); + if (derivedValue) { + Log.info(`Derived value for ${key} restored from cache`); + } - const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { - dependencyValues[i] = value; - }; - const checkAndMarkConnectionInitialized = (index: number) => { - if (connectionInitializedFlags.at(index)) { - return; - } + const setDependencyValue = (i: Index, value: Parameters[0][Index]) => { + dependencyValues[i] = value; + }; + const checkAndMarkConnectionInitialized = (index: number) => { + if (connectionInitializedFlags.at(index)) { + return; + } - connectionInitializedFlags[index] = true; - connectionsEstablishedCount++; - if (connectionsEstablishedCount === totalConnections) { - areAllConnectionsSet = true; - Log.info(`[OnyxDerived] All connections initialized for key: ${key}`); - } - }; + connectionInitializedFlags[index] = true; + connectionsEstablishedCount++; + if (connectionsEstablishedCount === totalConnections) { + areAllConnectionsSet = true; + Log.info(`[OnyxDerived] All connections initialized for key: ${key}`); + } + }; - // Create context once outside the function, swap values inline to avoid overhead of creating new objects frequently - const context: DerivedValueContext = { - currentValue: undefined, - sourceValues: undefined, - }; + // Create context once outside the function, swap values inline to avoid overhead of creating new objects frequently + const context: DerivedValueContext = { + currentValue: undefined, + sourceValues: undefined, + }; - const recomputeDerivedValue = (sourceKey?: string, sourceValue?: unknown, triggeredByIndex?: number) => { - // If this recompute was triggered by a connection callback, check if it initializes the connection - if (!areAllConnectionsSet && triggeredByIndex !== undefined) { - checkAndMarkConnectionInitialized(triggeredByIndex); - } + const recomputeDerivedValue = (sourceKey?: string, sourceValue?: unknown, triggeredByIndex?: number) => { + // If this recompute was triggered by a connection callback, check if it initializes the connection + if (!areAllConnectionsSet && triggeredByIndex !== undefined) { + checkAndMarkConnectionInitialized(triggeredByIndex); + } - // Before all connections are established, don't write to Onyx. - // This prevents overwriting a valid disk-cached value with empty defaults, - // and avoids N-1 unnecessary Onyx writes during initialization. - // We still update dependencyValues via setDependencyValue so data accumulates correctly. - if (!areAllConnectionsSet) { - Log.info(`[OnyxDerived] not all connections set for ${key}, deferring Onyx write`); - return; - } + // Before all connections are established, don't write to Onyx. + // This prevents overwriting a valid disk-cached value with empty defaults, + // and avoids N-1 unnecessary Onyx writes during initialization. + // We still update dependencyValues via setDependencyValue so data accumulates correctly. + if (!areAllConnectionsSet) { + Log.info(`[OnyxDerived] not all connections set for ${key}, deferring Onyx write`); + return; + } - context.currentValue = derivedValue; - context.sourceValues = sourceKey && sourceValue !== undefined ? {[sourceKey]: sourceValue} : undefined; + context.currentValue = derivedValue; + context.sourceValues = sourceKey && sourceValue !== undefined ? {[sourceKey]: sourceValue} : undefined; - const spanId = `${CONST.TELEMETRY.SPAN_ONYX_DERIVED_COMPUTE}_${key}`; - startSpan(spanId, { - name: CONST.TELEMETRY.SPAN_ONYX_DERIVED_COMPUTE, - op: CONST.TELEMETRY.SPAN_ONYX_DERIVED_COMPUTE, - parentSpan: getSpan(CONST.TELEMETRY.SPAN_APP_STARTUP), - attributes: {derivedKey: key}, - }); + const spanId = `${CONST.TELEMETRY.SPAN_ONYX_DERIVED_COMPUTE}_${key}`; + startSpan(spanId, { + name: CONST.TELEMETRY.SPAN_ONYX_DERIVED_COMPUTE, + op: CONST.TELEMETRY.SPAN_ONYX_DERIVED_COMPUTE, + parentSpan: getSpan(CONST.TELEMETRY.SPAN_APP_STARTUP), + attributes: {derivedKey: key}, + }); - try { - // @ts-expect-error TypeScript can't confirm the shape of dependencyValues matches the compute function's parameters - const newDerivedValue = compute(dependencyValues, context); - Log.info(`[OnyxDerived] updating value for ${key} in Onyx`); - derivedValue = newDerivedValue; - setDerivedValue(key, derivedValue); - } finally { - endSpan(spanId); - } - }; + try { + // @ts-expect-error TypeScript can't confirm the shape of dependencyValues matches the compute function's parameters + const newDerivedValue = compute(dependencyValues, context); + Log.info(`[OnyxDerived] updating value for ${key} in Onyx`); + derivedValue = newDerivedValue; + setDerivedValue(key, derivedValue); + } finally { + endSpan(spanId); + } + }; - for (let i = 0; i < dependencies.length; i++) { - const dependencyIndex = i; - const dependencyOnyxKey = dependencies[dependencyIndex]; + for (let i = 0; i < dependencies.length; i++) { + const dependencyIndex = i; + const dependencyOnyxKey = dependencies[dependencyIndex]; - if (OnyxKeys.isCollectionKey(dependencyOnyxKey)) { - Onyx.connectWithoutView({ - key: dependencyOnyxKey, - waitForCollectionCallback: true, - callback: (value, collectionKey, sourceValue) => { - Log.info(`[OnyxDerived] dependency ${collectionKey} for derived key ${key} changed, recomputing`); - setDependencyValue(dependencyIndex, value as Parameters[0][typeof dependencyIndex]); - recomputeDerivedValue(dependencyOnyxKey, sourceValue, dependencyIndex); - }, - }); - } else if (dependencyOnyxKey === ONYXKEYS.NVP_PREFERRED_LOCALE) { - // Special case for locale, we want to recompute derived values when the locale change actually loads. - Onyx.connectWithoutView({ - key: ONYXKEYS.RAM_ONLY_ARE_TRANSLATIONS_LOADING, - callback: (value) => { - if (value ?? true) { - Log.info(`[OnyxDerived] translations are still loading, not recomputing derived value for ${key}`); - return; - } - Log.info(`[OnyxDerived] translations loaded, recomputing derived value for ${key}`); - const localeValue = IntlStore.getCurrentLocale(); - if (!localeValue) { - Log.info(`[OnyxDerived] No locale found for derived key ${key}, skipping recompute`); - return; - } - Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`); - setDependencyValue(dependencyIndex, localeValue as Parameters[0][typeof dependencyIndex]); - recomputeDerivedValue(dependencyOnyxKey, localeValue, dependencyIndex); - }, - }); - } else { - Onyx.connectWithoutView({ - key: dependencyOnyxKey, - callback: (value) => { - Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`); - setDependencyValue(dependencyIndex, value as Parameters[0][typeof dependencyIndex]); - // if the dependency is not a collection, pass the entire value as the source value - recomputeDerivedValue(dependencyOnyxKey, value, dependencyIndex); - }, - }); - } + if (OnyxKeys.isCollectionKey(dependencyOnyxKey)) { + Onyx.connectWithoutView({ + key: dependencyOnyxKey, + waitForCollectionCallback: true, + callback: (value, collectionKey, sourceValue) => { + Log.info(`[OnyxDerived] dependency ${collectionKey} for derived key ${key} changed, recomputing`); + setDependencyValue(dependencyIndex, value as Parameters[0][typeof dependencyIndex]); + recomputeDerivedValue(dependencyOnyxKey, sourceValue, dependencyIndex); + }, + }); + } else if (dependencyOnyxKey === ONYXKEYS.NVP_PREFERRED_LOCALE) { + // Special case for locale, we want to recompute derived values when the locale change actually loads. + Onyx.connectWithoutView({ + key: ONYXKEYS.RAM_ONLY_ARE_TRANSLATIONS_LOADING, + callback: (value) => { + if (value ?? true) { + Log.info(`[OnyxDerived] translations are still loading, not recomputing derived value for ${key}`); + return; + } + Log.info(`[OnyxDerived] translations loaded, recomputing derived value for ${key}`); + const localeValue = IntlStore.getCurrentLocale(); + if (!localeValue) { + Log.info(`[OnyxDerived] No locale found for derived key ${key}, skipping recompute`); + return; + } + Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`); + setDependencyValue(dependencyIndex, localeValue as Parameters[0][typeof dependencyIndex]); + recomputeDerivedValue(dependencyOnyxKey, localeValue, dependencyIndex); + }, + }); + } else { + Onyx.connectWithoutView({ + key: dependencyOnyxKey, + callback: (value) => { + Log.info(`[OnyxDerived] dependency ${dependencyOnyxKey} for derived key ${key} changed, recomputing`); + setDependencyValue(dependencyIndex, value as Parameters[0][typeof dependencyIndex]); + // if the dependency is not a collection, pass the entire value as the source value + recomputeDerivedValue(dependencyOnyxKey, value, dependencyIndex); + }, + }); } - }); + } } } diff --git a/src/setup/backgroundLocationTrackingTask/index.native.ts b/src/setup/backgroundLocationTrackingTask/index.native.ts index f8e668bb6607..38f5f59c2d28 100644 --- a/src/setup/backgroundLocationTrackingTask/index.native.ts +++ b/src/setup/backgroundLocationTrackingTask/index.native.ts @@ -19,8 +19,8 @@ defineTask(BACKGROUND_LOCATION_TRACKING_TASK // Use NetInfo.fetch() instead of the in-memory NetworkState.isOffline() because this // background task may run in a headless JS context (Android) where module-level state // in NetworkState.ts hasn't been populated via Onyx/NetInfo subscribers. - const [gpsDraftDetailsPromiseResult, netInfoState] = await Promise.all([OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS).catch(() => undefined), NetInfo.fetch()]); - const gpsDraftDetails = gpsDraftDetailsPromiseResult ?? undefined; + const gpsDraftDetails = OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS); + const netInfoState = await NetInfo.fetch(); const isOffline = netInfoState.isConnected === false; updateStartAddress(gpsDraftDetails?.gpsPoints ?? [], data.locations.at(0), isOffline); diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index 67d4e63997e6..48f6d966bbf3 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -447,7 +447,7 @@ describe('actions/PolicyCategory', () => { await waitForBatchedUpdates(); // Then the approval rule should be created with the tag name - const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + const updatedPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); expect(updatedPolicy?.rules?.expenseRules).toHaveLength(1); expect(updatedPolicy?.rules?.expenseRules?.[0]?.applyWhen?.[0]?.value).toBe(categoryName); @@ -507,7 +507,7 @@ describe('actions/PolicyCategory', () => { await waitForBatchedUpdates(); // Then the approval rule should be created with the tag name - const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + const updatedPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); expect(updatedPolicy?.rules?.expenseRules).toHaveLength(1); expect(updatedPolicy?.rules?.expenseRules?.[0]?.applyWhen?.[0]?.value).toBe(categoryName); @@ -562,7 +562,7 @@ describe('actions/PolicyCategory', () => { await waitForBatchedUpdates(); // Verify the category was created - const policyCategories = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`); + const policyCategories = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`); const newCategory = policyCategories?.[newCategoryName]; expect(newCategory?.name).toBe(newCategoryName); @@ -609,7 +609,7 @@ describe('actions/PolicyCategory', () => { await waitForBatchedUpdates(); // Verify the category was created - const policyCategories = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`); + const policyCategories = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`); const newCategory = policyCategories?.[newCategoryName]; expect(newCategory?.name).toBe(newCategoryName); diff --git a/tests/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index 78ecb9788fd6..59d647ccf508 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -227,7 +227,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); - let policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + let policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); // Tag list name is updated and pending expect(Object.keys(policyTags?.[oldTagListName] ?? {}).length).toBe(0); @@ -237,7 +237,7 @@ describe('actions/Policy', () => { mockFetch?.resume(); await waitForBatchedUpdates(); - policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(policyTags?.[newTagListName]?.pendingAction).toBeFalsy(); expect(Object.keys(policyTags?.[oldTagListName] ?? {}).length).toBe(0); }); @@ -270,7 +270,7 @@ describe('actions/Policy', () => { mockFetch?.resume(); await waitForBatchedUpdates(); - const policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(policyTags?.[newTagListName]).toBeFalsy(); expect(policyTags?.[oldTagListName]).toBeTruthy(); @@ -316,7 +316,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should appear optimistically with pending state so the user sees immediate feedback - const policyTagsOptimistic = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTagsOptimistic = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const newTagOptimistic = policyTagsOptimistic?.[tagListName]?.tags?.[newTagName]; expect(newTagOptimistic?.name).toBe(newTagName); expect(newTagOptimistic?.enabled).toBe(true); @@ -327,7 +327,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the pending state should be cleared after API success - const policyTagsSuccess = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTagsSuccess = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const newTagSuccess = policyTagsSuccess?.[tagListName]?.tags?.[newTagName]; expect(newTagSuccess?.errors).toBeFalsy(); expect(newTagSuccess?.pendingAction).toBeFalsy(); @@ -373,7 +373,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should have errors - const policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; expect(newTag?.errors).toBeTruthy(); }); @@ -412,7 +412,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should be created in a new list with pending state so the user sees immediate feedback - const policyTagsOptimistic = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTagsOptimistic = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tagListKeys = Object.keys(policyTagsOptimistic ?? {}); const firstTagList = tagListKeys.at(0); if (firstTagList != null) { @@ -427,7 +427,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the pending state should be cleared after API success - const policyTagsSuccess = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTagsSuccess = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tagListKeysSuccess = Object.keys(policyTagsSuccess ?? {}); const firstTagListSuccess = tagListKeysSuccess.at(0); if (firstTagListSuccess != null) { @@ -480,7 +480,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should appear optimistically with pending state so the user sees immediate feedback - const policyTagsOptimistic = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTagsOptimistic = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const newTagOptimistic = policyTagsOptimistic?.[tagListName]?.tags?.[newTagName]; expect(newTagOptimistic?.name).toBe(newTagName); expect(newTagOptimistic?.enabled).toBe(true); @@ -491,7 +491,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the pending state should be cleared after API success - const policyTagsSuccess = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTagsSuccess = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const newTagSuccess = policyTagsSuccess?.[tagListName]?.tags?.[newTagName]; expect(newTagSuccess?.errors).toBeFalsy(); expect(newTagSuccess?.pendingAction).toBeFalsy(); @@ -683,7 +683,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should be renamed optimistically with pending action - const optimisticPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const optimisticPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tags = optimisticPolicyTags?.[tagListName]?.tags; expect(tags?.[oldTagName]).toBeFalsy(); @@ -695,7 +695,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the pending action should be cleared after API success and the tag name should be updated - const successPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const successPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const successTags = successPolicyTags?.[tagListName]?.tags; expect(successTags?.[oldTagName]).toBeFalsy(); @@ -743,7 +743,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag name should be reverted and an error should be set - const failurePolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const failurePolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tags = failurePolicyTags?.[tagListName]?.tags; expect(tags?.[newTagName]).toBeFalsy(); @@ -785,7 +785,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then Onyx data should remain unchanged - const updatedPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const updatedPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(updatedPolicyTags).toEqual(existingPolicyTags); }); @@ -840,7 +840,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the approval rule should be updated with the new tag name - const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + const updatedPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); expect(updatedPolicy?.rules?.approvalRules).toHaveLength(1); expect(updatedPolicy?.rules?.approvalRules?.[0]?.applyWhen?.[0]?.value).toBe(newTagName); @@ -884,7 +884,7 @@ describe('actions/Policy', () => { }); // Then optimistic update should be applied - const optimisticPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const optimisticPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const optimisticTag = optimisticPolicyTags?.[tagListName]?.tags[newTagName]; expect(optimisticTag?.name).toBe(newTagName); @@ -896,7 +896,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); }); - const successPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const successPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const successTags = successPolicyTags?.[tagListName]?.tags; expect(successTags?.[oldTagName]).toBeFalsy(); @@ -923,7 +923,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the approval rule should be created with the tag name - const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + const updatedPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); expect(updatedPolicy?.rules?.approvalRules).toHaveLength(1); expect(updatedPolicy?.rules?.approvalRules?.[0]?.applyWhen?.[0]?.value).toBe(tagName); @@ -969,7 +969,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the approval rule should be created with the tag name - const updatedPolicy = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); + const updatedPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`); expect(updatedPolicy?.rules?.approvalRules).toHaveLength(1); expect(updatedPolicy?.rules?.approvalRules?.[0]?.id).toBe('rule-1'); @@ -2177,7 +2177,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should have updated GL code with pending fields - let updatedPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + let updatedPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(updatedPolicyTags?.[tagListName]?.tags[tagName]['GL Code']).toBe(newGLCode); expect(updatedPolicyTags?.[tagListName]?.tags[tagName].pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); @@ -2187,7 +2187,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then after API success, pending fields should be cleared - updatedPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + updatedPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(updatedPolicyTags?.[tagListName]?.tags[tagName]['GL Code']).toBe(newGLCode); expect(updatedPolicyTags?.[tagListName]?.tags[tagName].pendingAction).toBeUndefined(); @@ -2218,7 +2218,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should have empty GL code - const updatedPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const updatedPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(updatedPolicyTags?.[tagListName]?.tags[tagName]['GL Code']).toBe(emptyGLCode); expect(updatedPolicyTags?.[tagListName]?.tags[tagName].pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); @@ -2255,7 +2255,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Then the tag should be restored to original state with error - const updatedPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const updatedPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(updatedPolicyTags?.[tagListName]?.tags[tagName]['GL Code']).toBe(originalGLCode); expect(updatedPolicyTags?.[tagListName]?.tags[tagName].errors).toBeTruthy(); @@ -2290,7 +2290,7 @@ describe('actions/Policy', () => { }); // Then the tag should have updated GL code - const updatedPolicyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const updatedPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); expect(updatedPolicyTags?.[tagListName]?.tags[tagName]['GL Code']).toBe(newGLCode); // Check optimistic data - pendingAction should be set @@ -2351,12 +2351,12 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); - const policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tagList = Object.values(policyTags ?? {}).at(0); const newTag = tagList?.tags?.[newTagName]; expect(newTag?.name).toBe(newTagName); - const taskReport = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); + const taskReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); expect(taskReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); expect(taskReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); @@ -2406,12 +2406,12 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); - const policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tagList = Object.values(policyTags ?? {}).at(0); const newTag = tagList?.tags?.[newTagName]; expect(newTag?.name).toBe(newTagName); - const taskReport = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); + const taskReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); expect(taskReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); expect(taskReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); @@ -2462,12 +2462,12 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); - const policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tagList = Object.values(policyTags ?? {}).at(0); const newTag = tagList?.tags?.[newTagName]; expect(newTag?.name).toBe(newTagName); - const taskReport = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); + const taskReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); expect(taskReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.OPEN); expect(taskReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.OPEN); @@ -2519,12 +2519,12 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); - const policyTags = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); + const policyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`); const tagList = Object.values(policyTags ?? {}).at(0); const newTag = tagList?.tags?.[newTagName]; expect(newTag?.name).toBe(newTagName); - const taskReport = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); + const taskReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${fakeTaskReportID}`); expect(taskReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.OPEN); expect(taskReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.OPEN); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 9b2ecb5c06ee..a148ac3504a0 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -2195,7 +2195,7 @@ describe('actions/Report', () => { await waitForBatchedUpdates(); - const persistedRequests = await OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); + const persistedRequests = OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); expect(persistedRequests?.at(1)?.command).toBe(WRITE_COMMANDS.OPEN_REPORT); expect(persistedRequests?.at(2)?.command).toBe(WRITE_COMMANDS.DELETE_COMMENT); @@ -2239,7 +2239,7 @@ describe('actions/Report', () => { await waitForBatchedUpdates(); - const persistedRequests = await OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); + const persistedRequests = OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); @@ -2437,7 +2437,7 @@ describe('actions/Report', () => { await waitForBatchedUpdates(); - report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); expect(report?.lastMentionedTime).toBeUndefined(); }); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 5dc4686e67ed..4d1a7bd7f9cd 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -504,7 +504,7 @@ describe('Unread Indicators', () => { expect(unreadIndicator).toHaveLength(1); // Leave a comment as the current user and verify the indicator is removed - const report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); addComment({ report, notifyReportID: REPORT_ID, @@ -583,7 +583,7 @@ describe('Unread Indicators', () => { // Navigate to the chat and simulate leaving a comment from the current user .then(() => navigateToSidebarOption(0)) .then(async () => { - const report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); // Leave a comment as the current user addComment({ report, @@ -614,7 +614,7 @@ describe('Unread Indicators', () => { // This message is visible on the sidebar and the report screen, so there are two occurrences. expect(screen.getAllByText('Current User Comment 1').at(0)).toBeOnTheScreen(); - const report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); if (lastReportAction) { deleteReportComment(report, lastReportAction, [], undefined, undefined, ''); } @@ -638,7 +638,7 @@ describe('Unread Indicators', () => { await signInAndGetAppWithUnreadChat(); await navigateToSidebarOption(0); - const report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); addComment({report, notifyReportID: REPORT_ID, ancestors: [], text: 'Comment 1', timezoneParam: CONST.DEFAULT_TIME_ZONE, currentUserAccountID: USER_A_ACCOUNT_ID}); await waitForBatchedUpdates(); @@ -821,7 +821,7 @@ describe('Unread Indicators', () => { await signInAndGetAppWithUnreadChat(); await navigateToSidebarOption(0); - const report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); // When USER_A add a comment addComment({report, notifyReportID: REPORT_ID, ancestors: [], text: 'Current User Comment', timezoneParam: CONST.DEFAULT_TIME_ZONE, currentUserAccountID: USER_A_ACCOUNT_ID}); @@ -841,7 +841,7 @@ describe('Unread Indicators', () => { await waitForBatchedUpdates(); // Then the lastReadTime of report should same as last action from USER_B - const updatedReport = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); + const updatedReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`); expect(updatedReport?.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportAction9CreatedDate, 1)); }); }); diff --git a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx index 9221d9bcd43e..77b74d0fe728 100644 --- a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx +++ b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx @@ -485,7 +485,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Get initial tax amount from transaction - let transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + let transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); const initialTaxAmount = transaction?.taxAmount; expect(initialTaxAmount).toBeTruthy(); @@ -544,7 +544,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Get updated tax amount - transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); const updatedTaxAmount = transaction?.taxAmount; @@ -612,7 +612,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Verify initial tax with default rate (5%) - let transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + let transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); expect(transaction?.taxCode).toBe('taxRate1'); expect(transaction?.taxAmount).toBe(476); @@ -652,7 +652,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Get tax after currency change - transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); // Tax code should change to foreign default (taxRate2 - 10%) expect(transaction?.taxCode).toBe('taxRate2'); @@ -759,7 +759,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Read tax amount - should be zero since taxClaimablePercentage is not configured - const transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + const transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); // taxClaimablePercentage defaults to 0, so tax should calculate correctly consistently with how it is calculated in the backend expect(transaction?.taxAmount).toBeDefined(); @@ -862,7 +862,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Get initial tax - let transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + let transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); const initialTaxAmount = transaction?.taxAmount; const initialTaxCode = transaction?.taxCode; @@ -920,7 +920,7 @@ describe('IOURequestStepConfirmationPageTest', () => { await waitForBatchedUpdatesWithAct(); // Get updated tax - transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); + transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID}`); const updatedTaxAmount = transaction?.taxAmount; const updatedTaxCode = transaction?.taxCode; diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index 142bad735f98..0137ce320870 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -59,7 +59,7 @@ describe('OnyxDerived', () => { it('returns empty reports when dependencies are not set', async () => { await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes).toMatchObject({ reports: {}, }); @@ -69,7 +69,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReport.reportID}`, mockReport); await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes).toMatchObject({ reports: { @@ -84,7 +84,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReport.reportID}`, mockReport); await waitForBatchedUpdates(); - let derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + let derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes).toMatchObject({ reports: { @@ -96,7 +96,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReport.reportID}`, null); - derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes).toMatchObject({ reports: {}, @@ -107,7 +107,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReport.reportID}`, mockReport); await IntlStore.load(CONST.LOCALES.ES); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes).toMatchObject({ locale: 'es', @@ -160,7 +160,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); // Get initial computed value - const initialDerivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const initialDerivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); // Spy on generateReportAttributes - this function should NOT be called // when the optimization kicks in and skips the computation @@ -179,7 +179,7 @@ describe('OnyxDerived', () => { expect(generateReportAttributesSpy).not.toHaveBeenCalled(); // Get the computed value after login change - const derivedReportAttributesAfterLoginChange = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributesAfterLoginChange = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); // And the values should be preserved correctly expect(derivedReportAttributesAfterLoginChange).toEqual(initialDerivedReportAttributes); @@ -202,7 +202,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); // Get initial computed value reference - const initialDerivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const initialDerivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); // Change the displayName - this should trigger full recomputation await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { @@ -214,7 +214,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); // Get the computed value after displayName change - const derivedReportAttributesAfterDisplayNameChange = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributesAfterDisplayNameChange = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); // The computed value should not be the same object (new computation happened) expect(derivedReportAttributesAfterDisplayNameChange).not.toBe(initialDerivedReportAttributes); @@ -226,7 +226,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes?.reports[report.reportID].reportErrors).toEqual({}); }); @@ -259,7 +259,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); await waitForBatchedUpdates(); @@ -311,7 +311,7 @@ describe('OnyxDerived', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, reportActions); await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes?.reports[report.reportID].reportErrors).toEqual({ '1234567890': 'Error message 1', '1234567891': 'Error message 2', @@ -356,7 +356,7 @@ describe('OnyxDerived', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, reportActions); await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes?.reports[report.reportID].reportErrors).toEqual({ '1234567890': 'Error message 1', '1234567891': 'Error message 2', @@ -387,7 +387,7 @@ describe('OnyxDerived', () => { // --- Assertion 1: Propagation Works --- // The parent report should have an error RBR because the child IOU report has an error. - let derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + let derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes?.reports[parentReport.reportID].brickRoadStatus).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); // --- Action: Resolve Error --- @@ -397,7 +397,7 @@ describe('OnyxDerived', () => { // --- Assertion 2: RBR is Cleared --- // The parent report's RBR should be cleared now that the child's error is gone. - derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); expect(derivedReportAttributes?.reports[parentReport.reportID].brickRoadStatus).toBeUndefined(); }); }); @@ -412,7 +412,7 @@ describe('OnyxDerived', () => { it('returns empty object when dependencies are not set', async () => { await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toEqual({}); }); @@ -430,7 +430,7 @@ describe('OnyxDerived', () => { '3': workspaceCard3, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -452,7 +452,7 @@ describe('OnyxDerived', () => { '3': workspaceCard, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList?.['1']).toBeDefined(); expect(derivedCardList?.['1']).toMatchObject({cardID: 1}); @@ -469,7 +469,7 @@ describe('OnyxDerived', () => { '1': workspaceCard, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -483,7 +483,7 @@ describe('OnyxDerived', () => { '1': nonPersonalCard, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -502,7 +502,7 @@ describe('OnyxDerived', () => { '2': card2, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -528,7 +528,7 @@ describe('OnyxDerived', () => { '4': workspaceCard3, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -546,7 +546,7 @@ describe('OnyxDerived', () => { '1': workspaceCard, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -560,7 +560,7 @@ describe('OnyxDerived', () => { '1': nonPersonalCard, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -579,7 +579,7 @@ describe('OnyxDerived', () => { '2': card2, }); await waitForBatchedUpdates(); - const derivedCardList = await OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); + const derivedCardList = OnyxUtils.get(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST); expect(derivedCardList).toMatchObject({ '1': expect.objectContaining({cardID: 1}), @@ -652,7 +652,7 @@ describe('OnyxDerived', () => { it('returns empty object when dependencies are not set', async () => { await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos).toEqual({ reportsToSubmit: [], reportsToApprove: [], @@ -802,7 +802,7 @@ describe('OnyxDerived', () => { }); it('returns correct number of reports for each category', async () => { - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.reportsToSubmit).toHaveLength(4); expect(todos?.reportsToApprove).toHaveLength(3); @@ -811,7 +811,7 @@ describe('OnyxDerived', () => { }); it('includes correct report IDs in each category', async () => { - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); const submitReportIDs = todos?.reportsToSubmit.map((r) => r.reportID) ?? []; const approveReportIDs = todos?.reportsToApprove.map((r) => r.reportID) ?? []; @@ -825,7 +825,7 @@ describe('OnyxDerived', () => { }); it('excludes reports that do not match any category', async () => { - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); const allReportIDs = [ ...(todos?.reportsToSubmit.map((r) => r.reportID) ?? []), @@ -839,7 +839,7 @@ describe('OnyxDerived', () => { }); it('builds transactionsByReportID mapping correctly', async () => { - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.transactionsByReportID).toBeDefined(); const firstSubmitReportID = SUBMIT_REPORT_IDS.at(0) ?? ''; @@ -856,7 +856,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${secondTransactionID}`, createMockTransaction(secondTransactionID, reportID)); await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.transactionsByReportID[reportID]).toHaveLength(2); expect(todos?.transactionsByReportID[reportID]?.map((t) => t.transactionID)).toEqual(expect.arrayContaining([`trans_submit_${reportID}`, secondTransactionID])); @@ -872,7 +872,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportWithoutTransactions.reportID}`, reportWithoutTransactions); await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); // The report should still be categorized, but transactionsByReportID should be undefined expect(todos?.transactionsByReportID[reportWithoutTransactions.reportID]).toBeUndefined(); @@ -881,7 +881,7 @@ describe('OnyxDerived', () => { it('updates when report state changes', async () => { // Start with a report that can be submitted const reportID = SUBMIT_REPORT_IDS.at(0); - let todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + let todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.reportsToSubmit.map((r) => r.reportID)).toContain(reportID); // Change the report to submitted state @@ -891,7 +891,7 @@ describe('OnyxDerived', () => { }); await waitForBatchedUpdates(); - todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); // The report should no longer be in reportsToSubmit expect(todos?.reportsToSubmit.map((r) => r.reportID)).not.toContain(reportID); @@ -909,14 +909,14 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); await waitForBatchedUpdates(); - let todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + let todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.transactionsByReportID[reportID] ?? []).toEqual([]); const transactionID = `trans_${reportID}`; await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, createMockTransaction(transactionID, reportID)); await waitForBatchedUpdates(); - todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.transactionsByReportID[reportID]).toHaveLength(1); expect(todos?.transactionsByReportID[reportID]?.at(0)?.transactionID).toBe(transactionID); }); @@ -965,7 +965,7 @@ describe('OnyxDerived', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); expect(todos?.reportsToExport).toHaveLength(0); }); @@ -1022,7 +1022,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); // The report should appear in reportsToPay because primary login matches reimburser expect(todos?.reportsToPay).toHaveLength(1); @@ -1052,7 +1052,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); // The report should still appear in reportsToPay using session email fallback expect(todos?.reportsToPay).toHaveLength(1); @@ -1082,7 +1082,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); - const todos = await OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); + const todos = OnyxUtils.get(ONYXKEYS.DERIVED.TODOS); // The report should NOT appear in reportsToPay because secondary login doesn't match expect(todos?.reportsToPay).toHaveLength(0); diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index cb5903ef2b05..90f88c76bfb0 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -124,7 +124,7 @@ describe('PersistedRequests persistence guarantees', () => { return waitForBatchedUpdates().then(async () => { // FIX: processNextRequest() now always persists ongoingRequest to disk // via Onyx.multiSet, regardless of the persistWhenOngoing flag. - const diskOngoing = await OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS); + const diskOngoing = OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS); expect(diskOngoing).toEqual(expect.objectContaining({command: 'OpenReport'})); }); })); @@ -158,7 +158,7 @@ describe('PersistedRequests persistence guarantees', () => { // Read disk state directly to see what's actually persisted return waitForBatchedUpdates().then(async () => { - const diskRequests = await OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); + const diskRequests = OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); const diskArray = diskRequests ?? []; // FIX: processNextRequest() now persists the updated queue to disk diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index 71952a667a7e..7406a7997884 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -1049,8 +1049,8 @@ describe('Transaction', () => { saveWaypoint({transactionID, index, waypoint, isDraft: false, recentWaypointsList}); await waitForBatchedUpdates(); - const transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const updatedRecentWaypoints = await OnyxUtils.get(ONYXKEYS.NVP_RECENT_WAYPOINTS); + const transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const updatedRecentWaypoints = OnyxUtils.get(ONYXKEYS.NVP_RECENT_WAYPOINTS); expect(transaction?.comment?.waypoints?.[`waypoint${index}`]).toEqual(waypoint); expect(updatedRecentWaypoints?.[0]?.address).toBe('123 Main St'); @@ -1068,7 +1068,7 @@ describe('Transaction', () => { saveWaypoint({transactionID, index, waypoint, isDraft: false, recentWaypointsList}); await waitForBatchedUpdates(); - const updatedRecentWaypoints = await OnyxUtils.get(ONYXKEYS.NVP_RECENT_WAYPOINTS); + const updatedRecentWaypoints = OnyxUtils.get(ONYXKEYS.NVP_RECENT_WAYPOINTS); expect(updatedRecentWaypoints?.length ?? 0).toBe(0); }); @@ -1084,7 +1084,7 @@ describe('Transaction', () => { saveWaypoint({transactionID, index, waypoint, isDraft: true, recentWaypointsList}); await waitForBatchedUpdates(); - const transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`); + const transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`); expect(transaction?.amount).toBe(CONST.IOU.DEFAULT_AMOUNT); }); @@ -1117,7 +1117,7 @@ describe('Transaction', () => { saveWaypoint({transactionID, index, waypoint, isDraft: false, recentWaypointsList}); await waitForBatchedUpdates(); - const transaction = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const transaction = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); expect(transaction?.errorFields?.route ?? null).toBeNull(); expect(transaction?.routes?.route0?.distance ?? null).toBeNull(); expect(transaction?.routes?.route0?.geometry?.coordinates ?? null).toBeNull(); @@ -1264,12 +1264,12 @@ describe('Transaction', () => { await waitForBatchedUpdates(); // Then the RTER violation should be removed optimistically - const optimisticViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const optimisticViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(optimisticViolations).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); // And a dismissed violation report action should be added - const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); const reportActionValues = Object.values(reportActions ?? {}); expect(reportActionValues.length).toBe(1); @@ -1279,10 +1279,10 @@ describe('Transaction', () => { await mockFetch.resume(); await waitForBatchedUpdates(); - const finalViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const finalViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(finalViolations).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); - const finalReportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); + const finalReportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); const finalReportActionValues = Object.values(finalReportActions ?? {}); expect(finalReportActionValues.length).toBe(1); expect(finalReportActionValues.at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION); @@ -1309,12 +1309,12 @@ describe('Transaction', () => { await waitForBatchedUpdates(); // Then the RTER violation should be restored to original state - const failureViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const failureViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(failureViolations).toEqual(mockViolations); // And the dismissed violation report action should be removed - const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); expect(Object.keys(reportActions ?? {}).length).toBe(0); }); @@ -1335,7 +1335,7 @@ describe('Transaction', () => { await waitForBatchedUpdates(); // Then the violations should remain empty after filtering out RTER - const optimisticViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const optimisticViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(optimisticViolations).toEqual([]); @@ -1374,7 +1374,7 @@ describe('Transaction', () => { }); // And a dismissed violation report action should be added - const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); const reportActionValues = Object.values(reportActions ?? {}); expect(reportActionValues.length).toBe(1); @@ -1432,10 +1432,10 @@ describe('Transaction', () => { }); await waitForBatchedUpdates(); - const optimisticViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const optimisticViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(optimisticViolations).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); - const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); const reportActionValues = Object.values(reportActions ?? {}); expect(reportActionValues.length).toBe(1); expect(reportActionValues.at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION); @@ -1446,10 +1446,10 @@ describe('Transaction', () => { await mockFetch.resume(); await waitForBatchedUpdates(); - const finalViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const finalViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(finalViolations).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); - const finalReportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const finalReportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); const finalReportActionValues = Object.values(finalReportActions ?? {}); // The optimistic dismissed violation report action is removed on successful API response to avoid duplicates expect(finalReportActionValues.length).toBe(0); @@ -1500,10 +1500,10 @@ describe('Transaction', () => { await mockFetch.resume(); await waitForBatchedUpdates(); - const failureViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const failureViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(failureViolations).toEqual(mockViolations); - const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); expect(Object.keys(reportActions ?? {}).length).toBe(0); }); @@ -1530,7 +1530,7 @@ describe('Transaction', () => { }); await waitForBatchedUpdates(); - const optimisticViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const optimisticViolations = OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); expect(optimisticViolations).toEqual([]); await mockFetch.resume(); @@ -1590,7 +1590,7 @@ describe('Transaction', () => { expect(result.current[0]).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); }); - const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); const reportActionValues = Object.values(reportActions ?? {}); expect(reportActionValues.length).toBe(1); expect(reportActionValues.at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION); diff --git a/tests/unit/WorkspacesSettingsUtilsTest.ts b/tests/unit/WorkspacesSettingsUtilsTest.ts index 5bc0f064974a..846beb779f60 100644 --- a/tests/unit/WorkspacesSettingsUtilsTest.ts +++ b/tests/unit/WorkspacesSettingsUtilsTest.ts @@ -44,7 +44,7 @@ describe('WorkspacesSettingsUtils', () => { }); await waitForBatchedUpdates(); - const reportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const reportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); // eslint-disable-next-line rulesdir/no-default-id-values const result = getBrickRoadForPolicy(report?.reportID ?? '', reportAttributes?.reports); @@ -66,7 +66,7 @@ describe('WorkspacesSettingsUtils', () => { }); await waitForBatchedUpdates(); - const reportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const reportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); // eslint-disable-next-line rulesdir/no-default-id-values const result = getBrickRoadForPolicy(report?.reportID ?? '', reportAttributes?.reports); @@ -95,7 +95,7 @@ describe('WorkspacesSettingsUtils', () => { const reportIDs = Object.values(reports).map((report) => report.reportID); await waitForBatchedUpdates(); - const reportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const reportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); const result = getChatTabBrickRoadReportID(reportIDs, reportAttributes?.reports); @@ -117,7 +117,7 @@ describe('WorkspacesSettingsUtils', () => { const reportIDs = Object.values(reports).map((report) => report.reportID); await waitForBatchedUpdates(); - const reportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const reportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); const result = getChatTabBrickRoadReportID(reportIDs, reportAttributes?.reports); @@ -145,7 +145,7 @@ describe('WorkspacesSettingsUtils', () => { const reportIDs = Object.values(reports).map((report) => report.reportID); await waitForBatchedUpdates(); - const reportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const reportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); const result = getChatTabBrickRoad(reportIDs, reportAttributes?.reports); @@ -167,7 +167,7 @@ describe('WorkspacesSettingsUtils', () => { const reportIDs = Object.values(reports).map((report) => report.reportID); await waitForBatchedUpdates(); - const reportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const reportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); const result = getChatTabBrickRoad(reportIDs, reportAttributes?.reports); diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index f16dacfea154..5725dde1f07d 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -104,7 +104,7 @@ describe('canEditFieldOfMoneyRequest', () => { }); it('should return false for invoice report action if it is not outstanding report', async () => { - const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + const outstandingReportsByPolicyID = OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); const canEditReportField = canEditFieldOfMoneyRequest({ reportAction, @@ -118,7 +118,7 @@ describe('canEditFieldOfMoneyRequest', () => { it('should return true for invoice report action when there are outstanding reports', async () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${IOUReportID}`, outstandingExpenseReport); await waitForBatchedUpdates(); - const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + const outstandingReportsByPolicyID = OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); const canEditReportField = canEditFieldOfMoneyRequest({ reportAction, @@ -249,7 +249,7 @@ describe('canEditFieldOfMoneyRequest', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_1_ID}`, outstandingExpenseReport1); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_2_ID}`, outstandingExpenseReport2); await waitForBatchedUpdates(); - const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + const outstandingReportsByPolicyID = OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); // When the submitter tries to move an expense between reports const canEditReportField = canEditFieldOfMoneyRequest({ @@ -276,7 +276,7 @@ describe('canEditFieldOfMoneyRequest', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_1_ID}`, outstandingExpenseReport1); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_2_ID}`, outstandingExpenseReport2); await waitForBatchedUpdates(); - const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + const outstandingReportsByPolicyID = OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); // When a user tries to move an expense between reports const canEditReportField = canEditFieldOfMoneyRequest({ @@ -309,7 +309,7 @@ describe('canEditFieldOfMoneyRequest', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_1_ID}`, approvedReport1); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_2_ID}`, reimbursedReport2); await waitForBatchedUpdates(); - const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + const outstandingReportsByPolicyID = OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); // When trying to move an expense between reports const canEditReportField = canEditFieldOfMoneyRequest({ @@ -329,7 +329,7 @@ describe('canEditFieldOfMoneyRequest', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_1_ID}`, outstandingExpenseReport1); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${EXPENSE_OUTSTANDING_REPORT_2_ID}`, outstandingExpenseReport2); await waitForBatchedUpdates(); - const outstandingReportsByPolicyID = await OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); + const outstandingReportsByPolicyID = OnyxUtils.get(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); // When the submitter tries to move an expense between reports const canEditReportField = canEditFieldOfMoneyRequest({ From 784bf37077e3ea2b3b9fa69b5e2a321e6b0b3744 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 22 Jul 2026 15:40:05 +0200 Subject: [PATCH 04/13] Onyx GET BaseReportActionContextMenu --- .../BaseReportActionContextMenu.tsx | 146 +++++++++--------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index f02a125ee5bc..a367609893fe 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -66,6 +66,7 @@ import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import {deepEqual} from 'fast-equals'; import React, {memo, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; @@ -187,7 +188,6 @@ function BaseReportActionContextMenu({ return originalReportActions[reportActionID]; }, [originalReportActions, reportActionID]); const transactionID = getLinkedTransactionID(reportAction); - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); const unapprovedOriginalID = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS) ? getOriginalMessage(reportAction)?.originalID @@ -201,15 +201,9 @@ function BaseReportActionContextMenu({ const [lhnOneTransactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(lhnOneTransactionThreadReportID)}`); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`); const harvestReportOriginalID = getNonEmptyStringOnyxID(getHarvestOriginalReportID(reportNameValuePairs?.origin, reportNameValuePairs?.originalID)); - const [harvestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${harvestReportOriginalID}`, {}); const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); const isOriginalReportArchived = useReportIsArchived(originalReportID); const policyID = report?.policyID; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); - - const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.FROM)}`); - const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.TO)}`); const sourceID = getSourceIDFromReportAction(reportAction); @@ -250,22 +244,13 @@ function BaseReportActionContextMenu({ const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${childReport?.parentReportID}`); const iouTransactionID = (getOriginalMessage(moneyRequestAction ?? reportAction) as OriginalMessageIOU | undefined)?.IOUTransactionID; const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`); - const [iouTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getNonEmptyStringOnyxID(iouTransactionID)}`); const iouReportID = (moneyRequestAction ?? reportAction)?.reportID; const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); const [moneyRequestPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport?.policyID}`); const {transactions} = useTransactionsAndViolationsForReport(childReport?.reportID); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const personalDetails = usePersonalDetails(); const reportAttributes = useReportAttributes(); const delegateAccountID = useDelegateAccountID(); - const isTrackIntentUser = isTrackOnboardingChoice(introSelected?.choice); - - const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; const session = useSession(); const encryptedAuthToken = session?.encryptedAuthToken ?? ''; @@ -286,8 +271,6 @@ function BaseReportActionContextMenu({ const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); const isHarvestReport = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID); const memberChangeLogReportActionMessage = isMemberChangeAction(reportAction) ? getOriginalMessage(reportAction) : undefined; - const [memberChangeLogRoomReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(`${memberChangeLogReportActionMessage?.reportID}`)}`); - const memberChangeLogRoomReportName = deprecatedGetReportName(memberChangeLogRoomReport, reportAttributes) || memberChangeLogReportActionMessage?.roomName; let filteredContextMenuActions = ContextMenuActions.filter( (contextAction) => @@ -389,6 +372,64 @@ function BaseReportActionContextMenu({ // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); + const getContextMenuPayload = (): ContextMenuActionPayload => { + const tryNewDot = OnyxUtils.get(ONYXKEYS.NVP_TRY_NEW_DOT); + const introSelected = OnyxUtils.get(ONYXKEYS.NVP_INTRO_SELECTED); + const guidedSetupAndTourStatus = guidedSetupAndTourStatusSelector(OnyxUtils.get(ONYXKEYS.NVP_ONBOARDING)); + const memberChangeLogRoomReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(`${memberChangeLogReportActionMessage?.reportID}`)}` as const); + + return { + reportActions, + childReportActions, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + reportAction: (reportAction ?? null) as ReportAction, + reportID, + originalReportID, + report, + selection, + close: () => setShouldKeepOpen(false), + transitionActionSheetState, + openContextMenu: () => setShouldKeepOpen(true), + interceptAnonymousUser, + openOverflowMenu, + setIsEmojiPickerActive, + personalDetails, + isHarvestReport, + moneyRequestAction, + card, + originalReport, + isTryNewDotNVPDismissed: !!tryNewDot?.classicRedirect?.dismissed, + isTrackIntentUser: isTrackOnboardingChoice(introSelected?.choice), + childReport, + movedFromReport: OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.FROM)}` as const), + movedToReport: OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.TO)}` as const), + getLocalDateFromDatetime, + policy: OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const), + policyTags: OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}` as const), + translate, + harvestReport: OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${harvestReportOriginalID}` as const), + harvestReportOriginalID, + introSelected, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + betas, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + currentUserAccountID: currentUserPersonalDetails?.accountID, + currentUserPersonalDetails, + encryptedAuthToken, + iouTransaction, + iouTransactionViolations: OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getNonEmptyStringOnyxID(iouTransactionID)}` as const), + bankAccountList: OnyxUtils.get(ONYXKEYS.BANK_ACCOUNT_LIST), + isOffline, + conciergeReportID: OnyxUtils.get(ONYXKEYS.CONCIERGE_REPORT_ID), + delegateAccountID, + reportAttributes, + originalReportOfUnapprovedTransaction, + memberChangeLogRoomReportName: deprecatedGetReportName(memberChangeLogRoomReport, reportAttributes) || memberChangeLogReportActionMessage?.roomName, + }; + }; + const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: enableEdgeToEdgeBottomSafeAreaPadding, style: wrapperStyle}); return ( @@ -401,66 +442,15 @@ function BaseReportActionContextMenu({ > {filteredContextMenuActions.map((contextAction, index) => { const closePopup = !isMini; - const payload: ContextMenuActionPayload = { - reportActions, - childReportActions, - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - reportAction: (reportAction ?? null) as ReportAction, - reportID, - originalReportID, - report, - selection, - close: () => setShouldKeepOpen(false), - transitionActionSheetState, - openContextMenu: () => setShouldKeepOpen(true), - interceptAnonymousUser, - openOverflowMenu, - setIsEmojiPickerActive, - personalDetails, - isHarvestReport, - moneyRequestAction, - card, - originalReport, - isTryNewDotNVPDismissed, - isTrackIntentUser, - childReport, - movedFromReport, - movedToReport, - getLocalDateFromDatetime, - policy, - policyTags, - translate, - harvestReport, - harvestReportOriginalID, - introSelected, - isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, - hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, - betas, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - currentUserAccountID: currentUserPersonalDetails?.accountID, - currentUserPersonalDetails, - encryptedAuthToken, - iouTransaction, - iouTransactionViolations, - bankAccountList, - isOffline, - conciergeReportID, - delegateAccountID, - reportAttributes, - originalReportOfUnapprovedTransaction, - memberChangeLogRoomReportName, - }; if ('renderContent' in contextAction) { - return contextAction.renderContent(closePopup, payload); + return contextAction.renderContent(closePopup, getContextMenuPayload()); } const {textTranslateKey} = contextAction; const isKeyInActionUpdateKeys = textTranslateKey === 'reportActionContextMenu.editAction' || textTranslateKey === 'reportActionContextMenu.deleteConfirmation'; const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: moneyRequestAction ?? reportAction}) : translate(textTranslateKey)); - const transactionPayload = textTranslateKey === 'reportActionContextMenu.copyMessage' && transaction && {transaction}; const isMenuAction = textTranslateKey === 'reportActionContextMenu.menu'; const successIcon = contextAction.successIcon ? icons[contextAction.successIcon] : undefined; @@ -474,10 +464,18 @@ function BaseReportActionContextMenu({ isMini={isMini} key={contextAction.textTranslateKey} onPress={(event) => - interceptAnonymousUser( - () => contextAction.onPress?.(closePopup, {...payload, ...transactionPayload, event, ...(isMenuAction ? {anchorRef: threeDotRef} : {})}), - contextAction.isAnonymousAction, - ) + interceptAnonymousUser(() => { + const transaction = + textTranslateKey === 'reportActionContextMenu.copyMessage' + ? OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}` as const) + : undefined; + contextAction.onPress?.(closePopup, { + ...getContextMenuPayload(), + ...(transaction && {transaction}), + event, + ...(isMenuAction ? {anchorRef: threeDotRef} : {}), + }); + }, contextAction.isAnonymousAction) } description={contextAction.getDescription?.(selection) ?? ''} isAnonymousAction={contextAction.isAnonymousAction} From 311c45984ac0f17ce3e437663da03e9140b295de Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 08:29:06 +0200 Subject: [PATCH 05/13] Onyx GET InboxTabButton --- .../NavigationTabBar/InboxTabButton.tsx | 61 ++++--------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx b/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx index cf4ee6622a6e..0348a2dde77b 100644 --- a/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx +++ b/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx @@ -2,8 +2,6 @@ import {PressableWithFeedback} from '@components/Pressable'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useRootNavigationState from '@hooks/useRootNavigationState'; import {useSidebarOrderedReportsState} from '@hooks/useSidebarOrderedReports'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,12 +16,11 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {Report, ReportActions} from '@src/types/onyx'; -import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import React from 'react'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import getLastRoute from './getLastRoute'; import NAVIGATION_TABS from './NAVIGATION_TABS'; @@ -54,55 +51,19 @@ type InboxTabButtonProps = { isWideLayout: boolean; }; -function doesLastReportExistSelector(report: OnyxEntry) { - return !!report?.reportID; -} - -function makeDoesLastReportActionExistSelector(actionID: string | undefined) { - return (reportActions: OnyxEntry) => { - const reportAction = actionID ? reportActions?.[actionID] : undefined; - return !!reportAction && !isDeletedAction(reportAction); - }; -} - type WideInboxTabButtonProps = { selectedTab: ValueOf; statusIndicatorColor: string | undefined; accessibilityLabel: string; }; -// The last-viewed report deep link only exists in the wide layout, so the report and report-action -// Onyx subscriptions live here and are only created when the wide layout is rendered. In the narrow -// layout tapping Inbox always routes to ROUTES.INBOX, so these subscriptions are never set up. +// The last-viewed report deep link only exists in the wide layout. In the narrow layout tapping +// Inbox always routes to ROUTES.INBOX. function WideInboxTabButton({selectedTab, statusIndicatorColor, accessibilityLabel}: WideInboxTabButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Inbox']); - const lastReportRouteReportID = useRootNavigationState((rootState) => { - if (!rootState) { - return undefined; - } - const route = getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT); - return getStringParam(route?.params, 'reportID'); - }); - - const lastReportRouteReportActionID = useRootNavigationState((rootState) => { - if (!rootState) { - return undefined; - } - const route = getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT); - return getStringParam(route?.params, 'reportActionID'); - }); - - const [doesLastReportExist] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${lastReportRouteReportID}`, {selector: doesLastReportExistSelector}, [lastReportRouteReportID]); - - const doesLastReportActionExistSelector = makeDoesLastReportActionExistSelector(lastReportRouteReportActionID); - const [doesLastReportActionExist] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${lastReportRouteReportID}`, {selector: doesLastReportActionExistSelector}, [ - lastReportRouteReportID, - lastReportRouteReportActionID, - ]); - const navigateToChats = () => { if (selectedTab === NAVIGATION_TABS.INBOX) { return; @@ -110,15 +71,19 @@ function WideInboxTabButton({selectedTab, statusIndicatorColor, accessibilityLab startNavigateToInboxTabSpan({isWideLayout: true}); - if (doesLastReportExist) { - // Fetch route params on-demand to avoid storing the full route object in render-time state - const rootState = navigationRef.getRootState(); - const lastRoute = rootState ? getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT) : undefined; - if (lastRoute) { - const reportID = getStringParam(lastRoute.params, 'reportID'); + // Fetch route params on-demand to avoid storing the full route object in render-time state + const rootState = navigationRef.getRootState(); + const lastRoute = rootState ? getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT) : undefined; + if (lastRoute) { + const reportID = getStringParam(lastRoute.params, 'reportID'); + const doesLastReportExist = !!OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}` as const)?.reportID; + if (doesLastReportExist) { const reportActionID = getStringParam(lastRoute.params, 'reportActionID'); const referrer = getStringParam(lastRoute.params, 'referrer'); const backTo = getStringParam(lastRoute.params, 'backTo'); + const reportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}` as const); + const reportAction = reportActionID ? reportActions?.[reportActionID] : undefined; + const doesLastReportActionExist = !!reportAction && !isDeletedAction(reportAction); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, doesLastReportActionExist ? reportActionID : undefined, referrer, backTo)); return; } From 20eed22d3e0051b44088f663a540d8fd23d1438a Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 08:39:04 +0200 Subject: [PATCH 06/13] Onyx GET useComposerSubmit --- .../ComposerWithSuggestions.tsx | 6 +-- .../getComposerReportData.ts | 54 +++++++++++++++++++ .../getLastEditableAction.ts | 31 +++++++++++ .../useComposerReportData.ts | 38 ------------- .../ReportActionCompose/useComposerSubmit.ts | 30 +++++------ .../ReportActionCompose/useEditMessage.ts | 7 +-- .../useLastEditableAction.ts | 33 ------------ 7 files changed, 106 insertions(+), 93 deletions(-) create mode 100644 src/pages/inbox/report/ReportActionCompose/getComposerReportData.ts create mode 100644 src/pages/inbox/report/ReportActionCompose/getLastEditableAction.ts delete mode 100644 src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts delete mode 100644 src/pages/inbox/report/ReportActionCompose/useLastEditableAction.ts diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx index 1eff778dc45c..8086af8d34e8 100644 --- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx @@ -76,13 +76,13 @@ import type {SuggestionsRef} from './ReportActionCompose'; import {useComposerActions, useComposerEditState, useComposerText} from './ComposerContext'; import getCursorPosition from './getCursorPosition'; +import getLastEditableAction from './getLastEditableAction'; import getScrollPosition from './getScrollPosition'; import getUpdatedSyncSelection from './getUpdatedSyncSelection'; import ReportActionComposeUtils from './ReportActionComposeUtils'; import SilentCommentUpdater from './SilentCommentUpdater'; import Suggestions from './Suggestions'; import useEditComposerToggle from './useEditComposerToggle'; -import useLastEditableAction from './useLastEditableAction'; type SyncSelection = { position: number; @@ -250,7 +250,6 @@ function ComposerWithSuggestions({ // Fullstory forwardedFSClass, }: ComposerWithSuggestionsProps) { - const lastReportAction = useLastEditableAction(reportID); const route = useRoute(); const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); @@ -631,6 +630,7 @@ function ComposerWithSuggestions({ const isEmptyComment = !valueRef.current || !!valueRef.current.match(CONST.REGEX.EMPTY_COMMENT); if (webEvent.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && selection.start <= 0 && isEmptyComment && !includeChronos) { webEvent.preventDefault(); + const lastReportAction = getLastEditableAction(reportID, route.name); if (lastReportAction) { const message = Array.isArray(lastReportAction?.message) ? (lastReportAction?.message?.at(-1) ?? null) : (lastReportAction?.message ?? null); saveReportActionDraft(reportID, lastReportAction, Parser.htmlToMarkdown(message?.html ?? '')); @@ -680,7 +680,7 @@ function ComposerWithSuggestions({ selection.end, includeChronos, onEnterKeyPress, - lastReportAction, + route.name, reportID, updateComment, setCurrentEditMessageSelection, diff --git a/src/pages/inbox/report/ReportActionCompose/getComposerReportData.ts b/src/pages/inbox/report/ReportActionCompose/getComposerReportData.ts new file mode 100644 index 000000000000..faaf0a18dee8 --- /dev/null +++ b/src/pages/inbox/report/ReportActionCompose/getComposerReportData.ts @@ -0,0 +1,54 @@ +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; +import {getIsOffline} from '@libs/NetworkState'; +import {getContinuousChain} from '@libs/PaginationUtils'; +import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, getSortedReportActionsForDisplay, isSentMoneyReportAction} from '@libs/ReportActionsUtils'; +import {canUserPerformWriteAction, isArchivedReport} from '@libs/ReportUtils'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction, Transaction} from '@src/types/onyx'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; + +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; + +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; + +type ComposerReportData = { + report: OnyxEntry; + filteredReportActions: ReportAction[]; + effectiveTransactionThreadReportID: string | undefined; +}; + +/** + * Synchronous, event-time equivalent of the composer's report data. Reads from the Onyx cache + * so it can be called inside event handlers without any render-bound subscriptions. + */ +function getComposerReportData(reportID: string): ComposerReportData { + const isOffline = getIsOffline(); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}` as const); + const chatReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}` as const); + + const nonEmptyStringReportID = getNonEmptyStringOnyxID(report?.reportID); + const isReportArchived = !!isArchivedReport(OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}` as const)); + const hasWriteAccess = canUserPerformWriteAction(report, isReportArchived); + const allReportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${nonEmptyStringReportID}` as const); + const sortedAllReportActions = getSortedReportActionsForDisplay(allReportActions, hasWriteAccess, true, undefined, nonEmptyStringReportID); + const reportActionPages = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${nonEmptyStringReportID}` as const); + const unfilteredReportActions = sortedAllReportActions.length + ? getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID).data + : []; + const filteredReportActions = getFilteredReportActionsForReportView(unfilteredReportActions); + + const allReportTransactions = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS)?.[reportID]?.transactions ?? getEmptyObject>(); + const reportTransactions = getAllNonDeletedTransactions(allReportTransactions, filteredReportActions, isOffline, true); + const visibleTransactions = isOffline ? reportTransactions : reportTransactions?.filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const reportTransactionIDs = visibleTransactions?.map((t) => t.transactionID); + const isSentMoneyReport = filteredReportActions.some((action) => isSentMoneyReportAction(action)); + const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, filteredReportActions, isOffline, reportTransactionIDs); + const effectiveTransactionThreadReportID = isSentMoneyReport ? undefined : transactionThreadReportID; + + return {report, filteredReportActions, effectiveTransactionThreadReportID}; +} + +export default getComposerReportData; diff --git a/src/pages/inbox/report/ReportActionCompose/getLastEditableAction.ts b/src/pages/inbox/report/ReportActionCompose/getLastEditableAction.ts new file mode 100644 index 000000000000..b98840388d98 --- /dev/null +++ b/src/pages/inbox/report/ReportActionCompose/getLastEditableAction.ts @@ -0,0 +1,31 @@ +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import {getCombinedReportActions, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {canEditReportAction} from '@libs/ReportUtils'; + +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; + +import type {OnyxEntry} from 'react-native-onyx'; + +import {getParentReportActionSelector} from '@selectors/ReportAction'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; + +import getComposerReportData from './getComposerReportData'; + +function getLastEditableAction(reportID: string, routeName: string): OnyxEntry { + const {report, filteredReportActions, effectiveTransactionThreadReportID} = getComposerReportData(reportID); + + const parentReportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.parentReportID)}` as const); + const parentReportAction = getParentReportActionSelector(parentReportActions, report?.parentReportActionID); + const transactionThreadReportActionsOnyx = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${effectiveTransactionThreadReportID}` as const); + const transactionThreadReportActionsArray = transactionThreadReportActionsOnyx ? Object.values(transactionThreadReportActionsOnyx) : []; + const combinedReportActions = getCombinedReportActions(filteredReportActions, effectiveTransactionThreadReportID ?? null, transactionThreadReportActionsArray); + + const isOnSearchMoneyRequestReport = routeName === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT; + const actionsForLastEditable = isOnSearchMoneyRequestReport ? filteredReportActions : combinedReportActions; + + return [...actionsForLastEditable, parentReportAction].find((action) => !isMoneyRequestAction(action) && canEditReportAction(action, undefined)); +} + +export default getLastEditableAction; diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts b/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts deleted file mode 100644 index 4bff522a7cdc..000000000000 --- a/src/pages/inbox/report/ReportActionCompose/useComposerReportData.ts +++ /dev/null @@ -1,38 +0,0 @@ -import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; -import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; - -import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; -import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, isSentMoneyReportAction} from '@libs/ReportActionsUtils'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction} from '@src/types/onyx'; - -import type {OnyxEntry} from 'react-native-onyx'; - -type ComposerReportData = { - report: OnyxEntry; - filteredReportActions: ReportAction[]; - effectiveTransactionThreadReportID: string | undefined; -}; - -function useComposerReportData(reportID: string): ComposerReportData { - const {isOffline} = useNetwork(); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`); - const {reportActions: unfilteredReportActions} = usePaginatedReportActions(report?.reportID); - const filteredReportActions = getFilteredReportActionsForReportView(unfilteredReportActions); - const allReportTransactions = useReportTransactionsCollection(reportID); - const reportTransactions = getAllNonDeletedTransactions(allReportTransactions, filteredReportActions, isOffline, true); - const visibleTransactions = isOffline ? reportTransactions : reportTransactions?.filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const reportTransactionIDs = visibleTransactions?.map((t) => t.transactionID); - const isSentMoneyReport = filteredReportActions.some((action) => isSentMoneyReportAction(action)); - const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, filteredReportActions, isOffline, reportTransactionIDs); - const effectiveTransactionThreadReportID = isSentMoneyReport ? undefined : transactionThreadReportID; - - return {report, filteredReportActions, effectiveTransactionThreadReportID}; -} - -export default useComposerReportData; diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts index 460df2194709..3f9a8ca33363 100644 --- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts +++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts @@ -1,8 +1,6 @@ -import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; -import useOnyx from '@hooks/useOnyx'; import {addAttachmentWithComment, addComment, clearAgentZeroProcessingIndicator} from '@libs/actions/Report'; import {createTaskAndNavigate, setNewOptimisticAssignee} from '@libs/actions/Task'; @@ -10,7 +8,7 @@ import {isEmailPublicDomain} from '@libs/LoginUtils'; import {rand64} from '@libs/NumberUtils'; import {addDomainToShortMention} from '@libs/ParsingUtils'; import {getAllPersonalDetailLogins, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; -import {isConciergeChatReport} from '@libs/ReportUtils'; +import {getAncestors, isConciergeChatReport} from '@libs/ReportUtils'; import {startSpan} from '@libs/telemetry/activeSpans'; import getSendMessageSource from '@libs/telemetry/getSendMessageSource'; import {generateAccountID} from '@libs/UserUtils'; @@ -27,9 +25,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useRoute} from '@react-navigation/native'; import {Str} from 'expensify-common'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import {useComposerActions, useComposerEditActions, useComposerEditState, useComposerMeta, useComposerSendState} from './ComposerContext'; -import useComposerReportData from './useComposerReportData'; +import getComposerReportData from './getComposerReportData'; import useSidePanelContext from './useSidePanelContext'; function useComposerSubmit(reportID: string) { @@ -37,9 +36,6 @@ function useComposerSubmit(reportID: string) { const isInSidePanel = useIsInSidePanel(); const sidePanelContext = useSidePanelContext(reportID); const route = useRoute(); - const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); - const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`); const delegateAccountID = useDelegateAccountID(); const {composerRef, attachmentFileRef, textRef} = useComposerMeta(); @@ -49,12 +45,6 @@ function useComposerSubmit(reportID: string) { const {publishDraft, setDidResetComposerHeightWhileEditing} = useComposerEditActions(); const {scrollOffsetRef} = useActionListContext(); - const {report, effectiveTransactionThreadReportID} = useComposerReportData(reportID); - const [targetReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${effectiveTransactionThreadReportID ?? reportID}`); - - const reportAncestors = useAncestors(report); - const targetReportAncestors = useAncestors(targetReport); - const currentUserEmail = currentUserPersonalDetails.email ?? ''; /** @@ -73,6 +63,14 @@ function useComposerSubmit(reportID: string) { return; } + const {report, effectiveTransactionThreadReportID} = getComposerReportData(reportID); + const targetReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${effectiveTransactionThreadReportID ?? reportID}` as const); + const conciergeReportID = OnyxUtils.get(ONYXKEYS.CONCIERGE_REPORT_ID); + const reportCollection = OnyxUtils.getCachedCollection(ONYXKEYS.COLLECTION.REPORT); + const reportDraftCollection = OnyxUtils.getCachedCollection(ONYXKEYS.COLLECTION.REPORT_DRAFT); + const reportActionsCollection = OnyxUtils.getCachedCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + const targetReportAncestors = getAncestors(targetReport, reportCollection, reportDraftCollection, reportActionsCollection); + // A new user message supersedes any Concierge processing indicator from a prior turn (e.g. a persisted // "...is working on your chat" while a human is handling it). Clear it optimistically so it disappears // the instant the user sends, instead of lingering until the ProcessAgentZeroRequest job runs; the @@ -145,8 +143,8 @@ function useComposerSubmit(reportID: string) { assigneeChatReport, policyID: report?.policyID, isCreatedUsingMarkdown: true, - quickAction, - ancestors: reportAncestors, + quickAction: OnyxUtils.get(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE), + ancestors: getAncestors(report, reportCollection, reportDraftCollection, reportActionsCollection), taskCreatorAndAssigneeDetails, }); return; @@ -187,7 +185,7 @@ function useComposerSubmit(reportID: string) { return; } - if (isComposerFullSize) { + if (OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}` as const)) { setIsComposerFullSize(reportID, false); } diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts index 8b11d34266ee..d1bfa75949f2 100644 --- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts +++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts @@ -1,7 +1,6 @@ import type {ComposerRef} from '@components/Composer/types'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useReportScrollManager from '@hooks/useReportScrollManager'; @@ -18,6 +17,8 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {DebouncedFuncLeading} from 'lodash'; import type React from 'react'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; + type UseEditMessageProps = { /** The report ID */ reportID: string | undefined; @@ -41,8 +42,6 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT const {email} = useCurrentUserPersonalDetails(); const actionOwnerReportID = originalReportID ?? reportID; - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${actionOwnerReportID}`); const isOriginalReportArchived = useReportIsArchived(actionOwnerReportID); const {stopEditing, submitEdit} = useReportActionActiveEditActions(); @@ -87,6 +86,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT submitEdit(); + const originalReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${actionOwnerReportID}` as const); + const personalDetails = OnyxUtils.get(ONYXKEYS.PERSONAL_DETAILS_LIST); editReportComment(originalReport, reportAction, trimmedNewDraft, isOriginalReportArchived, email ?? '', personalDetails, Object.fromEntries(draftMessageVideoAttributeCache)); deleteDraft(); } diff --git a/src/pages/inbox/report/ReportActionCompose/useLastEditableAction.ts b/src/pages/inbox/report/ReportActionCompose/useLastEditableAction.ts deleted file mode 100644 index 70c016c7f6d6..000000000000 --- a/src/pages/inbox/report/ReportActionCompose/useLastEditableAction.ts +++ /dev/null @@ -1,33 +0,0 @@ -import useOnyx from '@hooks/useOnyx'; -import useParentReportAction from '@hooks/useParentReportAction'; - -import {getCombinedReportActions, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {canEditReportAction} from '@libs/ReportUtils'; - -import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; - -import type {OnyxEntry} from 'react-native-onyx'; - -import {useRoute} from '@react-navigation/native'; - -import useComposerReportData from './useComposerReportData'; - -function useLastEditableAction(reportID: string): OnyxEntry { - const route = useRoute(); - - const {report, filteredReportActions, effectiveTransactionThreadReportID} = useComposerReportData(reportID); - - const parentReportAction = useParentReportAction(report); - const [transactionThreadReportActionsOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${effectiveTransactionThreadReportID}`); - const transactionThreadReportActionsArray = transactionThreadReportActionsOnyx ? Object.values(transactionThreadReportActionsOnyx) : []; - const combinedReportActions = getCombinedReportActions(filteredReportActions, effectiveTransactionThreadReportID ?? null, transactionThreadReportActionsArray); - - const isOnSearchMoneyRequestReport = route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || route.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT; - const actionsForLastEditable = isOnSearchMoneyRequestReport ? filteredReportActions : combinedReportActions; - - return [...actionsForLastEditable, parentReportAction].find((action) => !isMoneyRequestAction(action) && canEditReportAction(action, undefined)); -} - -export default useLastEditableAction; From 05085309ca553f0f244255e11ac5b27f5920caa1 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 09:45:25 +0200 Subject: [PATCH 07/13] Onyx GET ReportActionItemMessageWithExplain --- .../ReportActionItemMessageWithExplain.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx index 5abd088bfaca..758e5f75dfb0 100644 --- a/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx @@ -1,11 +1,7 @@ -import {usePersonalDetails} from '@components/OnyxListItemProvider'; import RenderHTML from '@components/RenderHTML'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {openLink} from '@libs/actions/Link'; @@ -21,8 +17,10 @@ import type {Report, ReportAction} from '@src/types/onyx'; import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import {delegateEmailSelector} from '@selectors/Account'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import ReportActionItemBasicMessage from './ReportActionItemBasicMessage'; @@ -46,14 +44,8 @@ type ReportActionItemMessageWithExplainProps = { */ function ReportActionItemMessageWithExplain({message, action, childReport, originalReport}: ReportActionItemMessageWithExplainProps) { const {translate} = useLocalize(); - const personalDetail = useCurrentUserPersonalDetails(); const {environmentURL} = useEnvironment(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const delegateAccountID = useDelegateAccountID(); - const personalDetails = usePersonalDetails(); const actionHasReasoning = hasReasoning(action); const computedMessage = actionHasReasoning ? `${message}${translate('iou.AskToExplain')}` : message; @@ -61,19 +53,26 @@ function ReportActionItemMessageWithExplain({message, action, childReport, origi const handleLinkPress = (event: GestureResponderEvent | KeyboardEvent, href: string) => { // Handle the special "Explain" link if (href.endsWith(CONST.CONCIERGE_EXPLAIN_LINK_PATH)) { - const participantsPersonalDetails = getParticipantsPersonalDetails([personalDetail.accountID, Number(action?.actorAccountID)], personalDetails); + const currentUserAccountID = OnyxUtils.get(ONYXKEYS.SESSION)?.accountID ?? CONST.DEFAULT_NUMBER_ID; + const personalDetails = OnyxUtils.get(ONYXKEYS.PERSONAL_DETAILS_LIST); + const participantsPersonalDetails = getParticipantsPersonalDetails([currentUserAccountID, Number(action?.actorAccountID)], personalDetails); + const introSelected = OnyxUtils.get(ONYXKEYS.NVP_INTRO_SELECTED); + const isSelfTourViewed = hasSeenTourSelector(OnyxUtils.get(ONYXKEYS.NVP_ONBOARDING)); + const betas = OnyxUtils.get(ONYXKEYS.BETAS); + const delegateEmail = delegateEmailSelector(OnyxUtils.get(ONYXKEYS.ACCOUNT)).toLowerCase(); + const delegateAccountID = delegateEmail ? Object.values(personalDetails ?? {}).find((detail) => detail?.login?.toLowerCase() === delegateEmail)?.accountID : undefined; explain( childReport, originalReport, action, translate, - personalDetail.accountID, + currentUserAccountID, introSelected, betas, isSelfTourViewed, delegateAccountID, participantsPersonalDetails, - personalDetail?.timezone, + personalDetails?.[currentUserAccountID]?.timezone, ); return; } From db62403ce9c4d1a2aeb4e1e2e0eb999a4117e825 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 09:45:47 +0200 Subject: [PATCH 08/13] Onyx GET ReportActionItem --- src/pages/inbox/report/ReportActionItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 8cd3388621fc..9e416d86aef3 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -85,6 +85,7 @@ import {deepEqual} from 'fast-equals'; import mapValues from 'lodash/mapValues'; import React, {useContext, useEffect, useRef, useState} from 'react'; import {Keyboard, View} from 'react-native'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import type {ContextMenuAnchor} from './ContextMenu/ReportActionContextMenu'; @@ -187,7 +188,6 @@ function ReportActionItem({ const reportID = report?.reportID ?? action?.reportID; const originalReportID = useOriginalReportID(report?.reportID, action); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getIOUReportIDFromReportActionPreview(action)}`, {selector: getStableReportSelector}); - const [iouPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`); const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector}); const transactionsOnIOUReport = useReportTransactions(iouReport?.reportID); @@ -256,6 +256,7 @@ function ReportActionItem({ const dismissError = () => { const transactionIDToDismiss = isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined; if (isSendingMoney && transactionIDToDismiss && reportID) { + const iouPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}` as const); cleanUpMoneyRequest(transactionIDToDismiss, action, reportID, transactionThreadReport, report, chatReport, undefined, originalReportID, true, iouPolicy); return; } From a579515641d0235a865b29c301a05eff98b83a42 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 09:46:00 +0200 Subject: [PATCH 09/13] Onyx GET ReportActionItemThread --- .../inbox/report/ReportActionItemThread.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/inbox/report/ReportActionItemThread.tsx b/src/pages/inbox/report/ReportActionItemThread.tsx index aada28fec0b3..beac64d1aa43 100644 --- a/src/pages/inbox/report/ReportActionItemThread.tsx +++ b/src/pages/inbox/report/ReportActionItemThread.tsx @@ -1,11 +1,8 @@ -import {usePersonalDetails} from '@components/OnyxListItemProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ReportActionAvatars from '@components/ReportActionAvatars'; import Text from '@components/Text'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {navigateToAndOpenChildReport} from '@libs/actions/Report'; @@ -21,6 +18,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; import {View} from 'react-native'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; type ReportActionItemThreadProps = { /** The current report */ @@ -44,13 +42,7 @@ type ReportActionItemThreadProps = { function ReportActionItemThread({report, reportAction, isHovered, onSecondaryInteraction, isEditingInline, isActive}: ReportActionItemThreadProps) { const styles = useThemeStyles(); - const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {translate, datetimeToCalendarTime} = useLocalize(); - const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportAction.childReportID}`); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const personalDetails = usePersonalDetails(); const numberOfReplies = reportAction.childVisibleActionCount ?? 0; const accountIDs = @@ -66,14 +58,22 @@ function ReportActionItemThread({report, reportAction, isHovered, onSecondaryInt const timeStamp = datetimeToCalendarTime(mostRecentReply, false); const wrapperStyle = isEditingInline ? styles.chatItemReactionsDraftRight : {}; + const handleOnPress = () => { + const currentUserAccountID = OnyxUtils.get(ONYXKEYS.SESSION)?.accountID ?? CONST.DEFAULT_NUMBER_ID; + const childReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportAction.childReportID}` as const); + const introSelected = OnyxUtils.get(ONYXKEYS.NVP_INTRO_SELECTED); + const isSelfTourViewed = hasSeenTourSelector(OnyxUtils.get(ONYXKEYS.NVP_ONBOARDING)); + const betas = OnyxUtils.get(ONYXKEYS.BETAS); + const personalDetails = OnyxUtils.get(ONYXKEYS.PERSONAL_DETAILS_LIST); + const participantsPersonalDetails = getParticipantsPersonalDetails([currentUserAccountID, Number(reportAction.actorAccountID)], personalDetails); + navigateToAndOpenChildReport(childReport, reportAction, report, currentUserAccountID, introSelected, betas, participantsPersonalDetails, isSelfTourViewed); + }; + return ( { - const participantsPersonalDetails = getParticipantsPersonalDetails([currentUserAccountID, Number(reportAction.actorAccountID)], personalDetails); - navigateToAndOpenChildReport(childReport, reportAction, report, currentUserAccountID, introSelected, betas, participantsPersonalDetails, isSelfTourViewed); - }} + onPress={handleOnPress} role={CONST.ROLE.BUTTON} accessibilityLabel={`${numberOfReplies} ${replyText}`} onSecondaryInteraction={onSecondaryInteraction} From 6a6444db8307f34dd6f293ac67d6a70952a2759c Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 10:46:36 +0200 Subject: [PATCH 10/13] Onyx GET TaskPreview --- .../ReportActionItem/TaskPreview.tsx | 8 +-- src/hooks/useHasOutstandingChildTask.ts | 51 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 40c30733ab4a..22f619ef92f9 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -9,7 +9,7 @@ import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; -import useHasOutstandingChildTask from '@hooks/useHasOutstandingChildTask'; +import {getHasOutstandingChildTask} from '@hooks/useHasOutstandingChildTask'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -44,6 +44,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {delegateEmailSelector} from '@selectors/Account'; import React from 'react'; import {View} from 'react-native'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & { /** The ID of the associated policy */ @@ -113,8 +114,6 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere const taskAssigneeAccountID = getTaskAssigneeAccountID(taskContextReport, parentReportAction) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const parentReport = useParentReport(taskContextReport?.reportID); const isParentReportArchived = useReportIsArchived(parentReport?.reportID); - const hasOutstandingChildTask = useHasOutstandingChildTask(taskContextReport); - const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const isTaskActionable = canActionTask(taskContextReport, parentReportAction, currentUserPersonalDetails.accountID, parentReport, isParentReportArchived); const hasAssignee = taskAssigneeAccountID > 0; const personalDetails = usePersonalDetails(); @@ -187,9 +186,12 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere shouldSelectOnPressEnter onPress={callFunctionIfActionIsAllowed(() => { updateTaskCheckboxStateForAccessibility(isTaskCompleted); + const delegateEmail = delegateEmailSelector(OnyxUtils.get(ONYXKEYS.ACCOUNT)); if (isTaskCompleted) { reopenTask(taskContextReport, parentReport, currentUserPersonalDetails.accountID, delegateEmail, taskReportID); } else { + const parentReportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskContextReport?.parentReportID}` as const); + const hasOutstandingChildTask = getHasOutstandingChildTask(taskContextReport, parentReportActions); completeTask(taskContextReport, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, delegateEmail, taskReportID); } })} diff --git a/src/hooks/useHasOutstandingChildTask.ts b/src/hooks/useHasOutstandingChildTask.ts index eebf60b508fe..891293195812 100644 --- a/src/hooks/useHasOutstandingChildTask.ts +++ b/src/hooks/useHasOutstandingChildTask.ts @@ -2,7 +2,7 @@ import {getReportActionMessage} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; +import type {Report, ReportActions} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -11,35 +11,44 @@ import {useMemo} from 'react'; import useOnyx from './useOnyx'; /** - * Hook to determine if a report has outstanding child tasks + * Determines if a report has outstanding child tasks based on the parent report's actions * @param taskReport - The task report to check + * @param reportActions - The report actions of the task report's parent report * @returns boolean indicating if there are outstanding child tasks */ -function useHasOutstandingChildTask(taskReport: OnyxEntry): boolean { - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReport?.parentReportID}`); +function getHasOutstandingChildTask(taskReport: OnyxEntry, reportActions: OnyxEntry): boolean { + if (!taskReport?.parentReportID || !reportActions) { + return false; + } - return useMemo(() => { - if (!taskReport?.parentReportID || !reportActions) { + return Object.values(reportActions).some((reportAction) => { + if (String(reportAction.childReportID) === String(taskReport?.reportID)) { return false; } - return Object.values(reportActions).some((reportAction) => { - if (String(reportAction.childReportID) === String(taskReport?.reportID)) { - return false; - } + if ( + reportAction.childType === CONST.REPORT.TYPE.TASK && + reportAction?.childStateNum === CONST.REPORT.STATE_NUM.OPEN && + reportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN && + !getReportActionMessage(reportAction)?.isDeletedParentAction + ) { + return true; + } - if ( - reportAction.childType === CONST.REPORT.TYPE.TASK && - reportAction?.childStateNum === CONST.REPORT.STATE_NUM.OPEN && - reportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN && - !getReportActionMessage(reportAction)?.isDeletedParentAction - ) { - return true; - } + return false; + }); +} - return false; - }); - }, [taskReport?.parentReportID, taskReport?.reportID, reportActions]); +/** + * Hook to determine if a report has outstanding child tasks + * @param taskReport - The task report to check + * @returns boolean indicating if there are outstanding child tasks + */ +function useHasOutstandingChildTask(taskReport: OnyxEntry): boolean { + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReport?.parentReportID}`); + + return useMemo(() => getHasOutstandingChildTask(taskReport, reportActions), [taskReport, reportActions]); } export default useHasOutstandingChildTask; +export {getHasOutstandingChildTask}; From 2c02cd76e07ea9bcecdc6fe8872ee1cbf7013cea Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 11:56:23 +0200 Subject: [PATCH 11/13] Onyx GET useOriginalReportID --- .../ReportActionCompose/ComposerProvider.tsx | 4 - .../getOriginalReportIDSync.ts | 75 +++++++++++++++++++ .../ReportActionCompose/useEditMessage.ts | 14 ++-- .../report/ReportActionItemMessageEdit.tsx | 1 - tests/unit/hooks/useEditMessage.test.ts | 1 - 5 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 src/pages/inbox/report/ReportActionCompose/getOriginalReportIDSync.ts diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx index 567298adb0f3..27c2984d0eed 100644 --- a/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx @@ -1,5 +1,4 @@ import useOnyx from '@hooks/useOnyx'; -import useOriginalReportID from '@hooks/useOriginalReportID'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -98,11 +97,8 @@ function ComposerProvider({children, reportID}: ComposerProviderProps) { debouncedCommentMaxLengthValidation.flush(); } - const originalReportID = useOriginalReportID(editingReportID ?? undefined, editingReportAction); - const {publishDraft, deleteDraft} = useEditMessage({ reportID: editingReportID ?? undefined, - originalReportID, reportAction: editingReportAction, shouldScrollToLastMessage: false, debouncedCommentMaxLengthValidation, diff --git a/src/pages/inbox/report/ReportActionCompose/getOriginalReportIDSync.ts b/src/pages/inbox/report/ReportActionCompose/getOriginalReportIDSync.ts new file mode 100644 index 000000000000..1c47d02bdd57 --- /dev/null +++ b/src/pages/inbox/report/ReportActionCompose/getOriginalReportIDSync.ts @@ -0,0 +1,75 @@ +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; +import {getIsOffline} from '@libs/NetworkState'; +import {getOneTransactionThreadReportID, withDEWRoutedActionsObject} from '@libs/ReportActionsUtils'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxInputOrEntry, ReportAction, Transaction} from '@src/types/onyx'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; + +import type {OnyxCollection} from 'react-native-onyx'; + +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; + +/** + * Synchronous, event-time equivalent of `useOriginalReportID`. Reads from the Onyx cache so it can be + * called inside event handlers without any render-bound subscriptions. + * + * This finds the "original reportID" for a given reportAction. The reportID usually is the report we are looking at, + * and in most cases it will be the same as the original reportID. However, in these cases the original reportID is different: + * - When viewing an expense report with a single transaction, the reportActions from the transaction thread and the expense report are merged, so in that case the + * reportAction's report may be different from the report we are viewing. + * - When viewing a thread report, the original reportID is the parent reportID, because the reportAction that created the thread belongs to the parent report. + * + * @param reportID The reportID of the report we are viewing + * @param reportAction The reportAction we want to find the original reportID for + * @returns The original reportID for the given reportAction, or undefined if not found + */ +function getOriginalReportIDSync(reportID: string | undefined, reportAction: OnyxInputOrEntry>): string | undefined { + const reportActions = withDEWRoutedActionsObject(OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}` as const)); + const report = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}` as const); + const chatReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}` as const); + const isOffline = getIsOffline(); + + const reportActionID = reportAction?.reportActionID; + const currentReportAction = reportActionID ? reportActions?.[reportActionID] : undefined; + const reportActionBelongsCurrentReport = Object.keys(currentReportAction ?? {}).length > 0; + const isThreadReportParentAction = reportAction?.childReportID?.toString() === reportID; + + if (!reportID) { + return undefined; + } + if (reportActionBelongsCurrentReport) { + // the reportActionID does belong to reportID + return reportID; + } + + if (isThreadReportParentAction) { + // This reportAction is the parent action of a thread report, so the original reportID is the parentReportID + return report?.parentReportID; + } + + if (reportActionID) { + // uniqueTransactionThreadReportID will only be found if the report with reportID is a report with a single transaction and we are merging reportActions + const allReportTransactions = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS)?.[reportID]?.transactions ?? getEmptyObject>(); + const visibleTransactionsIDs = getAllNonDeletedTransactions(allReportTransactions, Object.values(reportActions ?? {})) + .filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .map((transaction) => transaction.transactionID); + const uniqueTransactionThreadReportID = getOneTransactionThreadReportID({type: report?.type}, chatReport, reportActions ?? ([] as ReportAction[]), isOffline, visibleTransactionsIDs); + + // If we have a uniqueTransactionThreadReportID, then we are viewing an expense report with a single transaction and merging reportActions + // In that case, we need to check if the reportActionID belongs to the transaction thread. + if (uniqueTransactionThreadReportID) { + const uniqueTransactionThreadReportActions = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${uniqueTransactionThreadReportID}` as const); + const uniqueTransactionThreadReportAction = uniqueTransactionThreadReportActions?.[reportActionID]; + if (Object.keys(uniqueTransactionThreadReportAction ?? {}).length > 0) { + return uniqueTransactionThreadReportID; + } + } + } + + // If we reach here, we couldn't find the original reportID + return undefined; +} + +export default getOriginalReportIDSync; diff --git a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts index d1bfa75949f2..5fcf98ee0576 100644 --- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts +++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts @@ -1,10 +1,10 @@ import type {ComposerRef} from '@components/Composer/types'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useReportIsArchived from '@hooks/useReportIsArchived'; import useReportScrollManager from '@hooks/useReportScrollManager'; import {clearAllReportActionDrafts, editReportComment} from '@libs/actions/Report'; +import {isArchivedReport} from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {useReportActionActiveEditActions} from '@pages/inbox/report/ReportActionEditMessageContext'; @@ -19,11 +19,11 @@ import type React from 'react'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import getOriginalReportIDSync from './getOriginalReportIDSync'; + type UseEditMessageProps = { /** The report ID */ reportID: string | undefined; - /** The original report ID */ - originalReportID: string | undefined; /** The report action */ reportAction: OnyxTypes.ReportAction | null | undefined; /** Whether to scroll to the last message */ @@ -37,12 +37,10 @@ type UseEditMessageProps = { /** * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content. */ -function useEditMessage({reportID, originalReportID, reportAction, shouldScrollToLastMessage = false, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) { +function useEditMessage({reportID, reportAction, shouldScrollToLastMessage = false, debouncedCommentMaxLengthValidation, composerRef}: UseEditMessageProps) { const reportScrollManager = useReportScrollManager(); const {email} = useCurrentUserPersonalDetails(); - const actionOwnerReportID = originalReportID ?? reportID; - const isOriginalReportArchived = useReportIsArchived(actionOwnerReportID); const {stopEditing, submitEdit} = useReportActionActiveEditActions(); @@ -66,6 +64,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT * the new content. */ function publishDraft(draftMessage: string) { + console.log('publishDraft', draftMessage); if (!reportAction) { return; } @@ -77,6 +76,8 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT const trimmedNewDraft = draftMessage.trim(); + const actionOwnerReportID = getOriginalReportIDSync(reportID, reportAction) ?? reportID; + // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting. if (!trimmedNewDraft) { composerRef.current?.blur(); @@ -87,6 +88,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT submitEdit(); const originalReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${actionOwnerReportID}` as const); + const isOriginalReportArchived = !!isArchivedReport(OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${actionOwnerReportID}` as const)); const personalDetails = OnyxUtils.get(ONYXKEYS.PERSONAL_DETAILS_LIST); editReportComment(originalReport, reportAction, trimmedNewDraft, isOriginalReportArchived, email ?? '', personalDetails, Object.fromEntries(draftMessageVideoAttributeCache)); deleteDraft(); diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx index d1cc20fbdd9d..27ab670875b1 100644 --- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx @@ -257,7 +257,6 @@ function ReportActionItemMessageEdit({action, reportID, originalReportID, policy const {publishDraft, deleteDraft} = useEditMessage({ reportID, - originalReportID, reportAction: action, shouldScrollToLastMessage: index === 0, debouncedCommentMaxLengthValidation, diff --git a/tests/unit/hooks/useEditMessage.test.ts b/tests/unit/hooks/useEditMessage.test.ts index 11cba853d6d4..7b62996da165 100644 --- a/tests/unit/hooks/useEditMessage.test.ts +++ b/tests/unit/hooks/useEditMessage.test.ts @@ -102,7 +102,6 @@ describe('useEditMessage', () => { const props: HookProps = { reportID: report.reportID, - originalReportID: report.reportID, reportAction, debouncedCommentMaxLengthValidation: makeDebouncedValidator({flushResult: true}), composerRef: {current: {blur: jest.fn()} as never}, From 9de50fca1d60535260df16fbe4392359b01bee30 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Jul 2026 14:05:07 +0200 Subject: [PATCH 12/13] Onyx GET ChatTransactionPreview --- .../actionContents/ChatTransactionPreview.tsx | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx b/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx index fda9aa304606..8ca79c0a39c3 100644 --- a/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx +++ b/src/pages/inbox/report/actionContents/ChatTransactionPreview.tsx @@ -1,7 +1,5 @@ import TransactionPreview from '@components/ReportActionItem/TransactionPreview'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,6 +11,7 @@ import {getIOUReportIDFromReportActionPreview, isSplitBillAction, isTrackExpense import {createTransactionThreadReport} from '@userActions/Report'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -21,6 +20,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import React from 'react'; import {View} from 'react-native'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; type ChatTransactionPreviewProps = { /** All the data of the action, used for showing context menu and deriving the IOU report */ @@ -46,12 +46,38 @@ function ChatTransactionPreview({action, reportID, chatReport, iouReport, should const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const personalDetail = useCurrentUserPersonalDetails(); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [betas] = useOnyx(ONYXKEYS.BETAS); const reportPreviewStyles = StyleUtils.getMoneyRequestReportPreviewStyle(shouldUseNarrowLayout, 1, undefined, undefined); + const onPreviewPressed = () => { + if (shouldShowSplitPreview) { + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.SPLIT_BILL_DETAILS.getRoute(action.reportActionID))); + return; + } + + // If no childReportID exists, create transaction thread on-demand + if (!action.childReportID) { + const session = OnyxUtils.get(ONYXKEYS.SESSION); + const introSelected = OnyxUtils.get(ONYXKEYS.NVP_INTRO_SELECTED); + const betas = OnyxUtils.get(ONYXKEYS.BETAS); + const createdTransactionThreadReport = createTransactionThreadReport({ + introSelected, + currentUserLogin: session?.email ?? '', + currentUserAccountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + betas, + iouReport, + iouReportAction: action, + }); + if (createdTransactionThreadReport?.reportID) { + Navigation.navigate(getReportRouteForCurrentContext({reportID: createdTransactionThreadReport.reportID})); + return; + } + return; + } + + Navigation.navigate(getReportRouteForCurrentContext({reportID: action.childReportID})); + }; + return ( { - if (shouldShowSplitPreview) { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.SPLIT_BILL_DETAILS.getRoute(action.reportActionID))); - return; - } - - // If no childReportID exists, create transaction thread on-demand - if (!action.childReportID) { - const createdTransactionThreadReport = createTransactionThreadReport({ - introSelected, - currentUserLogin: personalDetail.email ?? '', - currentUserAccountID: personalDetail.accountID, - betas, - iouReport, - iouReportAction: action, - }); - if (createdTransactionThreadReport?.reportID) { - Navigation.navigate(getReportRouteForCurrentContext({reportID: createdTransactionThreadReport.reportID})); - return; - } - return; - } - - Navigation.navigate(getReportRouteForCurrentContext({reportID: action.childReportID})); - }} + onPreviewPressed={onPreviewPressed} isTrackExpense={isTrackExpenseAction(action)} /> From e67dc0f95d7a4bfed99a6cc018e08672a3af9d02 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Mon, 27 Jul 2026 09:16:40 +0200 Subject: [PATCH 13/13] Onyx GET transactionThreadReport --- .../MoneyRequestReportActionsList.tsx | 10 +- .../MoneyRequestReportView.tsx | 2 +- .../MoneyRequestReceiptView.tsx | 14 +- .../ReportActionItem/MoneyRequestView.tsx | 165 ++++++++++-------- .../ReportActionItemImage.tsx | 4 +- src/components/ReportActionItem/TaskView.tsx | 2 +- .../SearchList/ListItem/ChatListItem.tsx | 5 +- .../ShowContextMenuContext/default.ts | 5 +- .../ShowContextMenuContext/types.ts | 5 +- src/libs/ReportUtils.ts | 41 +++++ .../DebugReportActionCreatePage.tsx | 5 +- .../ReportAction/DebugReportActionPreview.tsx | 5 +- .../TransactionDuplicate/Confirmation.tsx | 6 +- src/pages/inbox/WideRHPReceiptPanel.tsx | 2 +- .../inbox/report/AncestorReportActionItem.tsx | 23 ++- .../report/MoneyReportContentCreated.tsx | 36 +++- src/pages/inbox/report/ReportActionItem.tsx | 24 ++- .../report/ReportActionItemContentCreated.tsx | 11 +- .../report/ReportActionItemParentAction.tsx | 29 ++- src/pages/inbox/report/ReportActionsList.tsx | 14 +- .../report/ReportActionsListItemRenderer.tsx | 25 ++- src/selectors/Report.ts | 23 +++ tests/ui/ClearReportActionErrorsUITest.tsx | 2 +- tests/ui/MoneyReportContentCreatedTest.tsx | 14 +- tests/ui/MoneyRequestViewReceiptTest.tsx | 6 +- tests/ui/MoneyRequestViewTest.tsx | 43 +++-- tests/ui/ReportActionItemTest.tsx | 68 ++++---- .../MoneyRequestReceiptViewTest.tsx | 13 +- 28 files changed, 415 insertions(+), 187 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 1c9778815139..aa19ffca769b 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -589,7 +589,10 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) parentReportAction={parentReportAction} parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread} report={reportStable} - transactionThreadReport={transactionThreadReport} + transactionThreadReportID={transactionThreadReport?.reportID} + transactionThreadPolicyID={transactionThreadReport?.policyID} + transactionThreadParentReportActionID={transactionThreadReport?.parentReportActionID} + transactionThreadParentReportID={transactionThreadReport?.parentReportID} chatReport={chatReport} displayAsGroup={displayAsGroup} shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID} @@ -609,7 +612,10 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) reportStable, chatReport, isOffline, - transactionThreadReport, + transactionThreadReport?.reportID, + transactionThreadReport?.policyID, + transactionThreadReport?.parentReportActionID, + transactionThreadReport?.parentReportID, unreadMarkerReportActionID, firstVisibleReportActionID, linkedReportActionID, diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 8013b3e8884b..e52cc4186187 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -267,7 +267,7 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport ; + /** The ID of the report currently being looked at */ + reportID: string | undefined; /** Whether we should show Money Request with disabled all fields */ readonly?: boolean; @@ -129,7 +133,7 @@ const receiptImageViolationNames = new Set([ const receiptFieldViolationNames = new Set([CONST.VIOLATIONS.MODIFIED_AMOUNT, CONST.VIOLATIONS.MODIFIED_DATE]); function MoneyRequestReceiptView({ - report, + reportID, readonly = false, updatedTransaction, fillSpace = false, @@ -137,6 +141,10 @@ function MoneyRequestReceiptView({ isDisplayedInWideRHP = false, hasParentPendingAction = false, }: MoneyRequestReceiptViewProps) { + // Real-time data from Onyx first, then the search-results snapshot for reports that only exist there (e.g. the merge-from-search flow). + const [reportFromOnyx] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`); + const {currentSearchResults} = useSearchResultsContext(); + const report = reportFromOnyx ?? (reportID ? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] : undefined); const styles = useThemeStyles(); const {translate} = useLocalize(); const {convertToDisplayString} = useCurrencyListActions(); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 37280338cbf2..104718885336 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -20,7 +20,6 @@ import useCardFeedErrors from '@hooks/useCardFeedErrors'; import useConfirmModal from '@hooks/useConfirmModal'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useDistanceRateOriginalPolicy from '@hooks/useDistanceRateOriginalPolicy'; import useEnvironment from '@hooks/useEnvironment'; import useHasMultipleSplitChildren from '@hooks/useHasMultipleSplitChildren'; @@ -57,6 +56,7 @@ import {getRateFromMerchant} from '@libs/MergeTransactionUtils'; import {isBillableEnabledOnPolicy, isSingleTransactionReport} from '@libs/MoneyRequestReportUtils'; import {hasEnabledOptions} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; +import Permissions from '@libs/Permissions'; import { canSubmitPerDiemExpenseFromWorkspace, findVendorByID, @@ -81,7 +81,7 @@ import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import { canEditFieldOfMoneyRequest, canEditMoneyRequest, - canUserPerformWriteAction as canUserPerformWriteActionReportUtils, + canUserPerformWriteActionOnFields, getTransactionDetails, getTripIDFromTransactionParentReportID, isExpenseReport, @@ -89,7 +89,7 @@ import { isOpenReport, isReportApproved, isSettled as isSettledReportUtils, - isTrackExpenseReportNew, + isTrackExpenseReportFromIDs, shouldEnableNegative, } from '@libs/ReportUtils'; import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils'; @@ -147,21 +147,31 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import {useRoute} from '@react-navigation/native'; +import {delegateEmailSelector} from '@selectors/Account'; import {isTrackIntentUserSelector} from '@selectors/Onboarding'; import {policyTypeSelector} from '@selectors/Policy'; +import {reportWritePermissionFieldsSelector} from '@selectors/Report'; import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {View} from 'react-native'; // Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot // eslint-disable-next-line no-restricted-imports import {useOnyx as originalUseOnyx} from 'react-native-onyx'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import MoneyRequestReceiptView from './MoneyRequestReceiptView'; type MoneyRequestViewProps = { - /** The report currently being looked at */ - transactionThreadReport?: OnyxEntry; + /** The ID of the transaction thread report */ + transactionThreadReportID?: string; + /** The policy ID of the transaction thread report */ + transactionThreadPolicyID?: string; + + /** The parent report action ID of the transaction thread report */ + transactionThreadParentReportActionID?: string; + + /** The parent report ID of the transaction thread report (the IOU/expense report) */ parentReportID?: string; /** Policy that the report belongs to */ @@ -195,7 +205,9 @@ const perDiemPoliciesSelector = (policies: OnyxCollection) => }; function MoneyRequestView({ - transactionThreadReport, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, parentReportID, expensePolicy, shouldShowAnimatedBackground, @@ -214,7 +226,6 @@ function MoneyRequestView({ const {convertToDisplayString, getCurrencySymbol} = useCurrencyListActions(); const {getReportRHPActiveRoute} = useActiveRoute(); const {showConfirmModal} = useConfirmModal(); - const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); const [loginToAccountIDMap] = useOnyx(ONYXKEYS.DERIVED.LOGIN_TO_ACCOUNT_ID_MAP); const {currentSearchResults} = useSearchResultsContext(); @@ -223,12 +234,12 @@ function MoneyRequestView({ // When this component is used when merging from the search page, we might not have the parent report stored in the main collection const [parentReportFromOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); const parentReport = parentReportFromOnyx ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; - const [parentReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(parentReport?.reportID)}`); - const [iouReportOwnerLogin] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsLoginSelector(parentReport?.ownerAccountID)}); - const [reportPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(parentReport?.policyID)}`); + // Identity-stable projection of the transaction thread report limited to the fields the + // write-permission check reads, so `isEditable` stays render-reactive without re-rendering on every send. + const [transactionThreadReportWriteFields] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {selector: reportWritePermissionFieldsSelector}); const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`); - const parentReportAction = transactionThreadReport?.parentReportActionID ? parentReportActions?.[transactionThreadReport.parentReportActionID] : undefined; + const parentReportAction = transactionThreadParentReportActionID ? parentReportActions?.[transactionThreadParentReportActionID] : undefined; const isFromMergeTransaction = !!mergeTransactionID; const linkedTransactionID = parentReportAction && isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; @@ -242,7 +253,8 @@ function MoneyRequestView({ const [policiesWithPerDiem] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { selector: perDiemPoliciesSelector, }); - const splitEffectivePolicy = useSplitEffectivePolicy(transactionThreadReport, undefined, transaction); + // Feed the field-picked projection: the hook only reads `policyID` from the report. + const splitEffectivePolicy = useSplitEffectivePolicy(transactionThreadReportWriteFields, undefined, transaction); const isPerDiemRequest = isPerDiemRequestTransactionUtils(transaction); const perDiemOriginalPolicy = getPolicyByCustomUnitID(transaction, policiesWithPerDiem); @@ -274,18 +286,14 @@ function MoneyRequestView({ const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST); const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); - const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`); const transactionViolations = useTransactionViolations(transaction?.transactionID, true, distanceOriginalPolicy ?? policy); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const delegateAccountID = useDelegateAccountID(); const personalDetailsList = usePersonalDetails(); - const currentUserAccountIDParam = currentUserPersonalDetails.accountID; const currentUserEmailParam = currentUserPersonalDetails.login ?? ''; const {isBetaEnabled} = usePermissions(); - const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const isP2PDistanceRequest = isCustomUnitRateIDForP2P(transaction); const moneyRequestReport = parentReport; const parentReportTransactions = useReportTransactions(moneyRequestReport?.reportID); @@ -295,7 +303,7 @@ function MoneyRequestView({ const visibleParentReportTransactions = parentReportTransactions.filter((t) => isOffline || !isTransactionPendingDelete(t)); const isApproved = isReportApproved({report: moneyRequestReport}); const isInvoice = isInvoiceReport(moneyRequestReport); - const isTrackExpense = !mergeTransactionID && isTrackExpenseReportNew(transactionThreadReport, moneyRequestReport, parentReportAction); + const isTrackExpense = !mergeTransactionID && isTrackExpenseReportFromIDs(parentReportID, transactionThreadParentReportActionID, moneyRequestReport, parentReportAction); let iouType: ValueOf; if (isTrackExpense) { @@ -379,10 +387,10 @@ function MoneyRequestView({ // Flags for allowing or disallowing editing an expense // Used for non-restricted fields such as: description, category, tag, billable, etc... - const isReportArchived = useReportIsArchived(transactionThreadReport?.reportID); - const isEditable = !!canUserPerformWriteActionReportUtils(transactionThreadReport, isReportArchived) && !readonly; + const isReportArchived = useReportIsArchived(transactionThreadReportID); + const isEditable = !!canUserPerformWriteActionOnFields(transactionThreadReportWriteFields, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, transaction, isChatReportArchived, moneyRequestReport, policy) && isEditable; - const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(transactionThreadReport?.policyID)}`; + const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(transactionThreadPolicyID)}`; const {personalCardsWithBrokenConnection} = useCardFeedErrors(); const connectionLink = getBrokenConnectionUrlToFixPersonalCard(personalCardsWithBrokenConnection, environmentURL); const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`); @@ -661,7 +669,26 @@ function MoneyRequestView({ !getPendingFieldAction('amount') && !pendingAction; + // Synchronous cache-only reads of the values used exclusively by the update handlers below. + // These were previously `useOnyx` subscriptions (or Onyx-backed hooks) whose values never + // affected render, so they're now read at press time instead of subscribing for them. + const getUpdateMoneyRequestHandlerParams = () => { + const transactionThreadReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` as const); + const parentReportNextStep = OnyxUtils.get(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(parentReport?.reportID)}` as const); + const reportPolicyTags = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(parentReport?.policyID)}` as const); + const personalDetails = OnyxUtils.get(ONYXKEYS.PERSONAL_DETAILS_LIST); + const iouReportOwnerLogin = personalDetailsLoginSelector(parentReport?.ownerAccountID)(personalDetails); + // Mirrors useDelegateAccountID: resolve the delegate's accountID from the account's delegate email. + const delegateEmail = delegateEmailSelector(OnyxUtils.get(ONYXKEYS.ACCOUNT)).toLowerCase(); + const delegateAccountID = delegateEmail ? Object.values(personalDetails ?? {}).find((detail) => detail?.login?.toLowerCase() === delegateEmail)?.accountID : undefined; + const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, OnyxUtils.get(ONYXKEYS.BETAS), OnyxUtils.get(ONYXKEYS.BETA_CONFIGURATION)); + const currentUserAccountIDParam = currentUserPersonalDetails.accountID; + return {transactionThreadReport, parentReportNextStep, reportPolicyTags, iouReportOwnerLogin, delegateAccountID, isASAPSubmitBetaEnabled, currentUserAccountIDParam}; + }; + const saveBillable = (newBillable: boolean) => { + const {transactionThreadReport, parentReportNextStep, reportPolicyTags, iouReportOwnerLogin, delegateAccountID, isASAPSubmitBetaEnabled, currentUserAccountIDParam} = + getUpdateMoneyRequestHandlerParams(); // If the value hasn't changed, don't request to save changes on the server and just close the modal if (newBillable === getBillable(transaction) || !transaction?.transactionID || !transactionThreadReport?.reportID) { return; @@ -687,6 +714,8 @@ function MoneyRequestView({ }; const saveReimbursable = (newReimbursable: boolean) => { + const {transactionThreadReport, parentReportNextStep, reportPolicyTags, iouReportOwnerLogin, delegateAccountID, isASAPSubmitBetaEnabled, currentUserAccountIDParam} = + getUpdateMoneyRequestHandlerParams(); // If the value hasn't changed, don't request to save changes on the server and just close the modal if (newReimbursable === getReimbursable(transaction) || !transaction?.transactionID || !transactionThreadReport?.reportID) { return; @@ -832,6 +861,8 @@ function MoneyRequestView({ return; } + const {transactionThreadReport, parentReportNextStep, iouReportOwnerLogin, delegateAccountID, isASAPSubmitBetaEnabled, currentUserAccountIDParam} = + getUpdateMoneyRequestHandlerParams(); updateMoneyRequestTaxRate({ transactionID: transaction?.transactionID, transactionThreadReport, @@ -868,6 +899,8 @@ function MoneyRequestView({ return; } + const {transactionThreadReport, parentReportNextStep, reportPolicyTags, iouReportOwnerLogin, delegateAccountID, isASAPSubmitBetaEnabled, currentUserAccountIDParam} = + getUpdateMoneyRequestHandlerParams(); updateMoneyRequestCategory({ transactionID, transactionThreadReport, @@ -904,6 +937,8 @@ function MoneyRequestView({ return; } + const {transactionThreadReport, parentReportNextStep, reportPolicyTags, iouReportOwnerLogin, delegateAccountID, isASAPSubmitBetaEnabled, currentUserAccountIDParam} = + getUpdateMoneyRequestHandlerParams(); // Clear only the pressed level so the other levels of a multi-level tag are kept. const updatedTag = insertTagIntoTransactionTagsString(transactionTag ?? '', '', tagListIndex, policy?.hasMultipleTagLists ?? false); updateMoneyRequestTag({ @@ -955,14 +990,12 @@ function MoneyRequestView({ shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} onPress={() => { - if (!transaction?.transactionID || !transactionThreadReport?.reportID) { + if (!transaction?.transactionID || !transactionThreadReportID) { return; } if (isOdometerDistanceRequest) { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport.reportID), - ); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReportID)); return; } @@ -972,7 +1005,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -980,13 +1013,7 @@ function MoneyRequestView({ } Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction.transactionID, - transactionThreadReport.reportID, - getReportRHPActiveRoute(), - ), + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReportID, getReportRHPActiveRoute()), ); }} brickRoadIndicator={getErrorForField('waypoints') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} @@ -1004,18 +1031,18 @@ function MoneyRequestView({ shouldShowRightIcon={canEditDistanceRate} titleStyle={styles.flex1} onPress={() => { - if (!transaction?.transactionID || !transactionThreadReport?.reportID) { + if (!transaction?.transactionID || !transactionThreadReportID) { return; } if (isTrackExpense) { - if (shouldNavigateToUpgradePath && transactionThreadReport) { + if (shouldNavigateToUpgradePath) { Navigation.navigate( ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ action: CONST.IOU.ACTION.EDIT, iouType, transactionID: transaction.transactionID, - reportID: transactionThreadReport?.reportID, + reportID: transactionThreadReportID, upgradePath: CONST.UPGRADE_PATHS.DISTANCE_RATES, }), ); @@ -1028,7 +1055,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, Navigation.getActiveRoute(), ), ), @@ -1038,13 +1065,7 @@ function MoneyRequestView({ } Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction.transactionID, - transactionThreadReport.reportID, - getReportRHPActiveRoute(), - ), + ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReportID, getReportRHPActiveRoute()), ); }} brickRoadIndicator={getErrorForField('customUnitRateID') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} @@ -1111,7 +1132,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEdit} titleStyle={styles.flex1} onPress={() => { - if (!transaction?.transactionID || !transactionThreadReport?.reportID) { + if (!transaction?.transactionID || !transactionThreadReportID) { return; } if (shouldShowTagDisabledAlert) { @@ -1124,7 +1145,7 @@ function MoneyRequestView({ iouType, orderWeight, transaction.transactionID, - transactionThreadReport.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -1158,7 +1179,7 @@ function MoneyRequestView({ const isInWideRHP = wideRHPRouteKeys.includes(route.key); // If the view is readonly, we don't need the transactionThread dependency - if ((!readonly && !transactionThreadReport?.reportID) || !transaction?.transactionID) { + if ((!readonly && !transactionThreadReportID) || !transaction?.transactionID) { return ; } @@ -1168,7 +1189,7 @@ function MoneyRequestView({ <> {(!isInWideRHP || isSmallScreenWidth || isFromReviewDuplicates || isFromMergeTransaction) && ( { - if (!transaction?.transactionID || !transactionThreadReport?.reportID) { + if (!transaction?.transactionID || !transactionThreadReportID) { return; } if (shouldShowSplitIndicator && isSplitAvailable) { + // `initSplitExpense` needs the whole report object, so sync-read it here. + const transactionThreadReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` as const); + const selfDMReportID = OnyxUtils.get(ONYXKEYS.SELF_DM_REPORT_ID); initSplitExpense(transaction, transactionThreadReport, splitEffectivePolicy, selfDMReportID, restrictedActionPolicyID, personalPolicy?.outputCurrency, { isProduction, }); @@ -1217,7 +1241,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport.reportID, + transactionThreadReportID, '', '', getReportRHPActiveRoute(), @@ -1245,7 +1269,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -1275,7 +1299,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -1300,13 +1324,7 @@ function MoneyRequestView({ titleStyle={styles.flex1} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction.transactionID, - transactionThreadReport?.reportID, - getReportRHPActiveRoute(), - ), + ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReportID, getReportRHPActiveRoute()), ); }} brickRoadIndicator={getErrorForField('date') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} @@ -1330,19 +1348,19 @@ function MoneyRequestView({ return; } - if (shouldNavigateToUpgradePath && transactionThreadReport) { + if (shouldNavigateToUpgradePath && transactionThreadReportID) { Navigation.navigate( ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ action: CONST.IOU.ACTION.EDIT, iouType, transactionID: transaction.transactionID, - reportID: transactionThreadReport?.reportID, + reportID: transactionThreadReportID, upgradePath: CONST.UPGRADE_PATHS.CATEGORIES, backTo: ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, Navigation.getActiveRoute(), ), }), @@ -1354,7 +1372,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, Navigation.getActiveRoute(), ), ), @@ -1365,7 +1383,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, Navigation.getActiveRoute(), ), ); @@ -1388,7 +1406,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEdit} titleStyle={styles.flex1} onPress={() => { - if (!transactionThreadReport?.reportID) { + if (!transactionThreadReportID) { return; } Navigation.navigate( @@ -1396,7 +1414,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -1440,7 +1458,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -1472,7 +1490,7 @@ function MoneyRequestView({ CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, getReportRHPActiveRoute(), ), ); @@ -1509,7 +1527,7 @@ function MoneyRequestView({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReportID)); }} brickRoadIndicator={getErrorForField('attendees') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('attendees')} @@ -1585,7 +1603,7 @@ function MoneyRequestView({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => { - if (!canEditReport || !transactionThreadReport) { + if (!canEditReport || !transactionThreadReportID) { return; } if (shouldNavigateToUpgradePath) { @@ -1594,18 +1612,19 @@ function MoneyRequestView({ iouType, action: CONST.IOU.ACTION.EDIT, transactionID: transaction?.transactionID, - reportID: transactionThreadReport?.reportID, + reportID: transactionThreadReportID, upgradePath: CONST.UPGRADE_PATHS.REPORTS, }), ); return; } + const lastVisitedPath = OnyxUtils.get(ONYXKEYS.LAST_VISITED_PATH); Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute( CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, - transactionThreadReport?.reportID, + transactionThreadReportID, getReportRHPActiveRoute() || lastVisitedPath, ), ); @@ -1642,9 +1661,9 @@ function MoneyRequestView({ onPress={() => { const reservations = transaction?.receipt?.reservationList?.length ?? 0; if (reservations > 1) { - Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(transactionThreadReport?.reportID, transaction.transactionID, getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(transactionThreadReportID, transaction.transactionID, getReportRHPActiveRoute())); } - Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(transactionThreadReport?.reportID, transaction.transactionID, '0', 0, getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(transactionThreadReportID, transaction.transactionID, '0', 0, getReportRHPActiveRoute())); }} /> )} diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 8d9d6a7a4a0a..804eddb75ccb 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -136,7 +136,7 @@ function ReportActionItemImage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Receipt']); - const {report: contextReport, transactionThreadReport} = useShowContextMenuState(); + const {report: contextReport, transactionThreadReportID} = useShowContextMenuState(); const isMapDistanceRequest = !!transaction && isDistanceRequest(transaction) && !isManualDistanceRequest(transaction); const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); // While the receipt is regenerating its stored URL is stale, so draw the live route from `routes.coordinates` @@ -146,7 +146,7 @@ function ReportActionItemImage({ deferReceiptNavigation(() => { Navigation.navigate( ROUTES.TRANSACTION_RECEIPT.getRoute( - transactionThreadReport?.reportID ?? contextReport?.reportID ?? reportProp?.reportID ?? getReportIDForExpense(transaction), + transactionThreadReportID ?? contextReport?.reportID ?? reportProp?.reportID ?? getReportIDForExpense(transaction), transaction?.transactionID, readonly, mergeTransactionID, diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 5404b8571de4..8c0a164f9e9a 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -113,7 +113,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { anchor: null, report, action, - transactionThreadReport: undefined, + transactionThreadReportID: undefined, isDisabled: true, shouldDisplayContextMenu: false, }), diff --git a/src/components/Search/SearchList/ListItem/ChatListItem.tsx b/src/components/Search/SearchList/ListItem/ChatListItem.tsx index 4e6ddf128769..882b80e85b9a 100644 --- a/src/components/Search/SearchList/ListItem/ChatListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ChatListItem.tsx @@ -89,7 +89,10 @@ function ChatListItem({ | null; report: OnyxEntry; action: OnyxEntry; - transactionThreadReport?: OnyxEntry; + transactionThreadReportID?: string; + transactionThreadPolicyID?: string; + transactionThreadParentReportActionID?: string; + transactionThreadParentReportID?: string; isDisabled: boolean; shouldDisplayContextMenu?: boolean; originalReportID?: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6b3342c66c1d..df1b7f6fabdd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1816,6 +1816,14 @@ function isThread(report: OnyxInputOrEntry): report is Thread { return !!(report?.parentReportID && report?.parentReportActionID); } +/** + * Returns true if the given parent IDs identify a thread (i.e. the report has a parent), without needing the report object itself. + * ID-based variant of `isThread`. + */ +function isThreadFromIDs(parentReportID: string | undefined, parentReportActionID: string | undefined): boolean { + return !!(parentReportID && parentReportActionID); +} + /** * Returns reportActions filtered to only policy expense chat reports (non-thread). */ @@ -2701,6 +2709,22 @@ function isTrackExpenseReportNew(report: OnyxInputOrEntry, parentReport: return false; } +/** + * ID-based variant of `isTrackExpenseReportNew`: same check, but the thread test is done on the report's + * parent IDs so callers don't need the transaction thread report object itself. + */ +function isTrackExpenseReportFromIDs( + parentReportID: string | undefined, + parentReportActionID: string | undefined, + parentReport: OnyxInputOrEntry, + parentReportAction: OnyxInputOrEntry, +): boolean { + if (isThreadFromIDs(parentReportID, parentReportActionID)) { + return !isEmptyObject(parentReportAction) && isSelfDM(parentReport) && isTrackExpenseAction(parentReportAction); + } + return false; +} + /** * Checks if a report is an IOU or expense request. */ @@ -10621,6 +10645,19 @@ function canUserPerformWriteAction(report: OnyxEntry, isReportArchived: ); } +/** Fields of a report that canUserPerformWriteAction actually reads. */ +type ReportWritePermissionFields = Pick; + +/** + * Field-limited variant of `canUserPerformWriteAction` so subscribers can use a projection that only + * contains the fields the check reads (see `reportWritePermissionFieldsSelector`) and stay + * identity-stable when unrelated report fields change. The cast is runtime-safe because + * `canUserPerformWriteAction` only reads the picked fields. + */ +function canUserPerformWriteActionOnFields(report: OnyxEntry, isReportArchived: boolean | undefined) { + return canUserPerformWriteAction(report as OnyxEntry, isReportArchived); +} + /** * Returns ID of the original report from which the given reportAction is first created. */ @@ -14182,11 +14219,15 @@ export { hasHeldExpensesFromTransactions, canMergeReports, canModifyHoldStatus, + isThreadFromIDs, + isTrackExpenseReportFromIDs, + canUserPerformWriteActionOnFields, }; export type { SortableColumnName, Ancestor, + ReportWritePermissionFields, DisplayNameWithTooltips, OptimisticAddCommentReportAction, OptimisticChatReport, diff --git a/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx b/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx index 2d283329802c..8a2837bb2941 100644 --- a/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx +++ b/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx @@ -137,7 +137,10 @@ function DebugReportActionCreatePage({ {!error && reportAction ? ( ({ - transactionThreadReport: report, + transactionThreadReportID: report?.reportID, action: reportAction, report, anchor: null, @@ -209,7 +209,9 @@ function Confirmation() { diff --git a/src/pages/inbox/report/AncestorReportActionItem.tsx b/src/pages/inbox/report/AncestorReportActionItem.tsx index 97c40ea31924..c6bc7fe948ff 100644 --- a/src/pages/inbox/report/AncestorReportActionItem.tsx +++ b/src/pages/inbox/report/AncestorReportActionItem.tsx @@ -71,8 +71,17 @@ type AncestorReportActionItemProps = { /** If the thread divider line will be used */ shouldUseThreadDividerLine: boolean; - /** The transaction thread report associated with the current report, if any */ - transactionThreadReport: OnyxEntry; + /** The ID of the transaction thread report associated with the current report, if any */ + transactionThreadReportID?: string; + + /** The policy ID of the transaction thread report */ + transactionThreadPolicyID?: string; + + /** The parent report action ID of the transaction thread report */ + transactionThreadParentReportActionID?: string; + + /** The parent report ID of the transaction thread report */ + transactionThreadParentReportID?: string; }; function AncestorReportActionItem({ @@ -91,7 +100,10 @@ function AncestorReportActionItem({ linkedTransactionRouteError, parentReportAction, shouldUseThreadDividerLine, - transactionThreadReport, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, + transactionThreadParentReportID, }: AncestorReportActionItemProps) { const styles = useThemeStyles(); const currentUserPersonalDetail = useCurrentUserPersonalDetails(); @@ -159,7 +171,10 @@ function AncestorReportActionItem({ action={reportAction} onPress={canOpenAncestorReport ? openAncestorReport : undefined} parentReportAction={parentReportAction} - transactionThreadReport={transactionThreadReport} + transactionThreadReportID={transactionThreadReportID} + transactionThreadPolicyID={transactionThreadPolicyID} + transactionThreadParentReportActionID={transactionThreadParentReportActionID} + transactionThreadParentReportID={transactionThreadParentReportID} chatReport={chatReport} displayAsGroup={false} shouldDisplayNewMarker={shouldDisplayNewMarker} diff --git a/src/pages/inbox/report/MoneyReportContentCreated.tsx b/src/pages/inbox/report/MoneyReportContentCreated.tsx index 42bba53109fd..c20b191ad1eb 100644 --- a/src/pages/inbox/report/MoneyReportContentCreated.tsx +++ b/src/pages/inbox/report/MoneyReportContentCreated.tsx @@ -28,8 +28,17 @@ type MoneyReportContentCreatedProps = { /** The transaction associated with the parent CREATED action, when applicable */ transaction: OnyxEntry; - /** The transaction-thread report, if its data has already been subscribed via `useOnyx` */ - transactionThreadReport: OnyxEntry; + /** The ID of the transaction-thread report, if any */ + transactionThreadReportID?: string; + + /** The policy ID of the transaction-thread report */ + transactionThreadPolicyID?: string; + + /** The parent report action ID of the transaction-thread report */ + transactionThreadParentReportActionID?: string; + + /** The parent report ID of the transaction-thread report */ + transactionThreadParentReportID?: string; /** The CREATED report action that this content belongs to */ action: OnyxEntry; @@ -41,7 +50,18 @@ type MoneyReportContentCreatedProps = { threadDivider: React.ReactNode; }; -function MoneyReportContentCreated({report, policy, transaction, transactionThreadReport, action, shouldHideThreadDividerLine, threadDivider}: MoneyReportContentCreatedProps) { +function MoneyReportContentCreated({ + report, + policy, + transaction, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, + transactionThreadParentReportID, + action, + shouldHideThreadDividerLine, + threadDivider, +}: MoneyReportContentCreatedProps) { const styles = useThemeStyles(); const reportTransactions = useReportTransactions(report?.reportID); const contextMenuStateValue = useShowContextMenuState(); @@ -54,9 +74,9 @@ function MoneyReportContentCreated({report, policy, transaction, transactionThre // `MoneyReportView` against the report's stale `total` (0) and flash "Total $0.00". When // we detect that state, forward `isTotalPending` so `MoneyReportView` renders its loading // indicator in place of the amount until the thread arrives. - const isPendingSingleExpenseThread = isSingleTransactionReport(report, reportTransactions) && !transactionThreadReport?.reportID; + const isPendingSingleExpenseThread = isSingleTransactionReport(report, reportTransactions) && !transactionThreadReportID; - const hasThread = !!transactionThreadReport?.reportID; + const hasThread = !!transactionThreadReportID; return ( @@ -76,8 +96,10 @@ function MoneyReportContentCreated({report, policy, transaction, transactionThre diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 9e416d86aef3..286cf7f7566f 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -106,8 +106,17 @@ type ReportActionItemProps = { /** Report for this action */ report: OnyxEntry; - /** The transaction thread report associated with the report for this action, if any */ - transactionThreadReport: OnyxEntry; + /** The ID of the transaction thread report associated with the report for this action, if any */ + transactionThreadReportID?: string; + + /** The policy ID of the transaction thread report */ + transactionThreadPolicyID?: string; + + /** The parent report action ID of the transaction thread report */ + transactionThreadParentReportActionID?: string; + + /** The parent report ID of the transaction thread report */ + transactionThreadParentReportID?: string; /** The chat report associated with the report for this action (report.chatReportID) */ chatReport: OnyxEntry; @@ -167,7 +176,10 @@ type ReportActionItemProps = { function ReportActionItem({ action, report, - transactionThreadReport, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, + transactionThreadParentReportID, chatReport, linkedReportActionID, displayAsGroup, @@ -256,6 +268,7 @@ function ReportActionItem({ const dismissError = () => { const transactionIDToDismiss = isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined; if (isSendingMoney && transactionIDToDismiss && reportID) { + const transactionThreadReport = OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` as const); const iouPolicy = OnyxUtils.get(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}` as const); cleanUpMoneyRequest(transactionIDToDismiss, action, reportID, transactionThreadReport, report, chatReport, undefined, originalReportID, true, iouPolicy); return; @@ -431,7 +444,10 @@ function ReportActionItem({ anchor: popoverAnchorRef, report, action, - transactionThreadReport, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, + transactionThreadParentReportID, isDisabled: false, shouldDisplayContextMenu: shouldDisplayContextMenuValue, originalReportID, diff --git a/src/pages/inbox/report/ReportActionItemContentCreated.tsx b/src/pages/inbox/report/ReportActionItemContentCreated.tsx index 8a1d86d7df37..9e90a0bb05b9 100644 --- a/src/pages/inbox/report/ReportActionItemContentCreated.tsx +++ b/src/pages/inbox/report/ReportActionItemContentCreated.tsx @@ -49,7 +49,7 @@ function ReportActionItemContentCreated({parentReportAction, transactionID, draf const {translate} = useLocalize(); const contextMenuStateValue = useShowContextMenuState(); const contextMenuActionsValue = useShowContextMenuActions(); - const {report, action, transactionThreadReport} = contextMenuStateValue; + const {report, action, transactionThreadReportID, transactionThreadPolicyID, transactionThreadParentReportActionID, transactionThreadParentReportID} = contextMenuStateValue; const policy = usePolicy(report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE ? undefined : report?.policyID); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID)}`); @@ -103,7 +103,9 @@ function ReportActionItemContentCreated({parentReportAction, transactionID, draf ; - /** The transaction thread report associated with the current report, if any */ - transactionThreadReport: OnyxEntry; + /** The ID of the transaction thread report associated with the current report, if any */ + transactionThreadReportID?: string; /** Report actions belonging to the report's parent */ parentReportAction: OnyxEntry; @@ -56,10 +57,26 @@ type ReportActionItemParentActionProps = { shouldUseThreadDividerLine?: boolean; }; +/** + * Picks only the transaction-thread IDs the item subtree needs, so ancestor rows don't re-render + * on unrelated transaction-thread report changes. + */ +const transactionThreadIDsSelector = (report: OnyxEntry) => { + if (!report?.reportID) { + return undefined; + } + return { + reportID: report.reportID, + policyID: report.policyID, + parentReportActionID: report.parentReportActionID, + parentReportID: report.parentReportID, + }; +}; + function ReportActionItemParentAction({ report, action, - transactionThreadReport, + transactionThreadReportID, parentReportAction, shouldHideThreadDividerLine = false, shouldDisplayReplyDivider, @@ -67,6 +84,7 @@ function ReportActionItemParentAction({ shouldUseThreadDividerLine = false, }: ReportActionItemParentActionProps) { const styles = useThemeStyles(); + const [transactionThreadIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionThreadReportID)}`, {selector: transactionThreadIDsSelector}); const ancestors = useAncestors(report, shouldExcludeAncestorReportAction); const transactionID = isMoneyRequestAction(action) && getOriginalMessage(action)?.IOUTransactionID; const [allBetas] = useOnyx(ONYXKEYS.BETAS); @@ -152,7 +170,10 @@ function ReportActionItemParentAction({ isReportArchived={isReportArchived} isSelfTourViewed={isSelfTourViewed} parentReportAction={parentReportAction} - transactionThreadReport={transactionThreadReport} + transactionThreadReportID={transactionThreadIDs?.reportID} + transactionThreadPolicyID={transactionThreadIDs?.policyID} + transactionThreadParentReportActionID={transactionThreadIDs?.parentReportActionID} + transactionThreadParentReportID={transactionThreadIDs?.parentReportID} isFirstVisibleReportAction={isFirstVisibleReportAction} shouldUseThreadDividerLine={shouldUseThreadDividerLine} linkedTransactionRouteError={linkedTransactionRouteError} diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 0e1cbd5f6b88..5055e308200c 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -162,8 +162,8 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps) selector: reportAttributesSelector, }); const isHarvestCreatedExpenseReportAction = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID); - - const [reportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {selector: getStableReportSelector}); + const getStableReportSelectorCall = (reportToSelect: OnyxEntry) => getStableReportSelector(reportToSelect); + const [reportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {selector: getStableReportSelectorCall}); const [chatReportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportStable?.chatReportID)}`, {selector: getStableReportSelector}); const linkedReportActionID = reportActionIDFromRoute; @@ -353,6 +353,11 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps) return isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report); })(); + const transactionThreadReportID = transactionThreadReport?.reportID; + const transactionThreadPolicyID = transactionThreadReport?.policyID; + const transactionThreadParentReportActionID = transactionThreadReport?.parentReportActionID; + const transactionThreadParentReportID = transactionThreadReport?.parentReportID; + const renderItem = ({item: reportAction, index}: ListRenderItemInfo) => { const shouldDisableContextMenuForConciergeDraft = draftReportActionID === reportAction.reportActionID; @@ -363,7 +368,10 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps) parentReportAction={parentReportAction} parentReportActionForTransactionThread={parentReportActionForTransactionThread} report={reportStable} - transactionThreadReport={transactionThreadReport} + transactionThreadReportID={transactionThreadReportID} + transactionThreadPolicyID={transactionThreadPolicyID} + transactionThreadParentReportActionID={transactionThreadParentReportActionID} + transactionThreadParentReportID={transactionThreadParentReportID} chatReport={chatReportStable} linkedReportActionID={linkedReportActionID} displayAsGroup={ diff --git a/src/pages/inbox/report/ReportActionsListItemRenderer.tsx b/src/pages/inbox/report/ReportActionsListItemRenderer.tsx index 1fa4b402d6c0..b9be47410247 100644 --- a/src/pages/inbox/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/inbox/report/ReportActionsListItemRenderer.tsx @@ -24,8 +24,17 @@ type ReportActionsListItemRendererProps = { /** Report for this action */ report: OnyxEntry; - /** The transaction thread report associated with the report for this action, if any */ - transactionThreadReport: OnyxEntry; + /** The ID of the transaction thread report associated with the report for this action, if any */ + transactionThreadReportID?: string; + + /** The policy ID of the transaction thread report */ + transactionThreadPolicyID?: string; + + /** The parent report action ID of the transaction thread report */ + transactionThreadParentReportActionID?: string; + + /** The parent report ID of the transaction thread report */ + transactionThreadParentReportID?: string; /** The chat report associated with the report for this action (report.chatReportID) */ chatReport?: OnyxEntry; @@ -65,7 +74,10 @@ function ReportActionsListItemRenderer({ reportAction, parentReportAction, report, - transactionThreadReport, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, + transactionThreadParentReportID, chatReport, displayAsGroup, shouldHideThreadDividerLine, @@ -163,7 +175,7 @@ function ReportActionsListItemRenderer({ reportID={report.reportID} report={report} action={action} - transactionThreadReport={transactionThreadReport} + transactionThreadReportID={transactionThreadReportID} isFirstVisibleReportAction={isFirstVisibleReportAction} shouldUseThreadDividerLine={shouldUseThreadDividerLine} /> @@ -175,7 +187,10 @@ function ReportActionsListItemRenderer({ shouldHideThreadDividerLine={shouldHideThreadDividerLine} parentReportAction={parentReportAction} report={report} - transactionThreadReport={transactionThreadReport} + transactionThreadReportID={transactionThreadReportID} + transactionThreadPolicyID={transactionThreadPolicyID} + transactionThreadParentReportActionID={transactionThreadParentReportActionID} + transactionThreadParentReportID={transactionThreadParentReportID} chatReport={chatReport} parentReportActionForTransactionThread={parentReportActionForTransactionThread} action={action} diff --git a/src/selectors/Report.ts b/src/selectors/Report.ts index cfa0b41246ba..8b8ba673a32a 100644 --- a/src/selectors/Report.ts +++ b/src/selectors/Report.ts @@ -1,4 +1,5 @@ import {getOriginalMessage, isClosedAction} from '@libs/ReportActionsUtils'; +import type {ReportWritePermissionFields} from '@libs/ReportUtils'; import { canShowReportRecipientLocalTime, getPolicyIDsWithEmptyReportsForAccount, @@ -209,6 +210,27 @@ function isDraftReportSelector(draft: OnyxEntry): boolean { return !!draft; } +/** + * Projection of exactly the fields `canUserPerformWriteActionOnFields` (ReportUtils) reads, so + * write-permission subscribers stay identity-stable when unrelated report fields (e.g. chat + * heartbeat `last*` fields) change. + */ +function reportWritePermissionFieldsSelector(report: OnyxEntry): ReportWritePermissionFields | undefined { + if (!report?.reportID) { + return undefined; + } + return { + reportID: report.reportID, + type: report.type, + parentReportID: report.parentReportID, + parentReportActionID: report.parentReportActionID, + permissions: report.permissions, + writeCapability: report.writeCapability, + policyID: report.policyID, + errorFields: report.errorFields, + }; +} + export { getArchiveReason, getReportChatType, @@ -220,6 +242,7 @@ export { openExpenseReportIDsSelector, getStableReportSelector, isDraftReportSelector, + reportWritePermissionFieldsSelector, }; export type {StableReport}; diff --git a/tests/ui/ClearReportActionErrorsUITest.tsx b/tests/ui/ClearReportActionErrorsUITest.tsx index ca9cc57a0ca0..dbf34342fbc2 100644 --- a/tests/ui/ClearReportActionErrorsUITest.tsx +++ b/tests/ui/ClearReportActionErrorsUITest.tsx @@ -91,7 +91,7 @@ describe('ClearReportActionErrors UI', () => { { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); }); - renderWithProps({report, transactionThreadReport: undefined}); + renderWithProps({report, transactionThreadReportID: undefined}); await waitForBatchedUpdatesWithAct(); const lastCall = mockMoneyReportView.mock.calls.at(-1)?.at(0); @@ -117,7 +117,7 @@ describe('MoneyReportContentCreated', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); }); - renderWithProps({report, transactionThreadReport: threadReport}); + renderWithProps({report, transactionThreadReportID: threadReport.reportID}); await waitForBatchedUpdatesWithAct(); // In the combined branch the inline `MoneyReportView` is invoked with `isCombinedReport` @@ -129,7 +129,7 @@ describe('MoneyReportContentCreated', () => { it('forwards isTotalPending=false when there are zero transactions', async () => { const report = buildExpenseReport(); - renderWithProps({report, transactionThreadReport: undefined}); + renderWithProps({report, transactionThreadReportID: undefined}); await waitForBatchedUpdatesWithAct(); const lastCall = mockMoneyReportView.mock.calls.at(-1)?.at(0); @@ -146,7 +146,7 @@ describe('MoneyReportContentCreated', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${t2.transactionID}`, t2); }); - renderWithProps({report, transactionThreadReport: undefined}); + renderWithProps({report, transactionThreadReportID: undefined}); await waitForBatchedUpdatesWithAct(); const lastCall = mockMoneyReportView.mock.calls.at(-1)?.at(0); @@ -161,7 +161,7 @@ describe('MoneyReportContentCreated', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${otherReportTransaction.transactionID}`, otherReportTransaction); }); - renderWithProps({report, transactionThreadReport: undefined}); + renderWithProps({report, transactionThreadReportID: undefined}); await waitForBatchedUpdatesWithAct(); const lastCall = mockMoneyReportView.mock.calls.at(-1)?.at(0); diff --git a/tests/ui/MoneyRequestViewReceiptTest.tsx b/tests/ui/MoneyRequestViewReceiptTest.tsx index 12f794ce263d..8e90143c92ed 100644 --- a/tests/ui/MoneyRequestViewReceiptTest.tsx +++ b/tests/ui/MoneyRequestViewReceiptTest.tsx @@ -134,7 +134,9 @@ const renderMoneyRequestView = () => render( { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [currentUserAccountID]: {accountID: currentUserAccountID, login: currentUserEmail, displayName: 'Test User'}, }); + // The component reads the transaction thread report from Onyx (by ID), so it must be seeded there. + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${threadReport.reportID}`, threadReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { id: policyID, type: CONST.POLICY.TYPE.TEAM, diff --git a/tests/ui/MoneyRequestViewTest.tsx b/tests/ui/MoneyRequestViewTest.tsx index f9463164445c..f6c33b3f5eeb 100644 --- a/tests/ui/MoneyRequestViewTest.tsx +++ b/tests/ui/MoneyRequestViewTest.tsx @@ -95,11 +95,17 @@ const expenseReportID = 'expense_mrv_123'; const parentReportActionID = 'parent_action_mrv'; const transactionID = 'txn_mrv_test'; -const renderMoneyRequestView = (threadReport: ReturnType, policy?: Record) => - render( +const renderMoneyRequestView = async (threadReport: ReturnType, policy?: Record) => { + // The component reads the transaction thread report from Onyx (by ID), so it must be seeded there. + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${threadReport.reportID}`, threadReport); + }); + return render( , ); +}; describe('MoneyRequestView edit fields', () => { beforeAll(() => { @@ -199,7 +206,7 @@ describe('MoneyRequestView edit fields', () => { await setupTestData(); - renderMoneyRequestView(threadReport); + await renderMoneyRequestView(threadReport); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -232,7 +239,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, {tax: {trackingEnabled: false}}); + await renderMoneyRequestView(threadReport, {tax: {trackingEnabled: false}}); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -251,7 +258,7 @@ describe('MoneyRequestView edit fields', () => { await setupTestData(); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, {tax: {trackingEnabled: false}}); + await renderMoneyRequestView(threadReport, {tax: {trackingEnabled: false}}); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -279,7 +286,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); + await renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -297,7 +304,7 @@ describe('MoneyRequestView edit fields', () => { await setupTestData(true); - renderMoneyRequestView(threadReport); + await renderMoneyRequestView(threadReport); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -325,7 +332,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport); + await renderMoneyRequestView(threadReport); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -356,7 +363,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport); + await renderMoneyRequestView(threadReport); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -389,7 +396,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport); + await renderMoneyRequestView(threadReport); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -422,7 +429,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport); + await renderMoneyRequestView(threadReport); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -451,7 +458,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); + await renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -479,7 +486,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); + await renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -508,7 +515,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); + await renderMoneyRequestView(threadReport, {tax: {trackingEnabled: true}}); await waitForBatchedUpdatesWithAct(); await waitFor(() => { @@ -534,7 +541,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, { + await renderMoneyRequestView(threadReport, { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, @@ -568,7 +575,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, { + await renderMoneyRequestView(threadReport, { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, @@ -601,7 +608,7 @@ describe('MoneyRequestView edit fields', () => { }); await waitForBatchedUpdatesWithAct(); - renderMoneyRequestView(threadReport, { + await renderMoneyRequestView(threadReport, { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, diff --git a/tests/ui/ReportActionItemTest.tsx b/tests/ui/ReportActionItemTest.tsx index 97aa17ba74e9..571365763829 100644 --- a/tests/ui/ReportActionItemTest.tsx +++ b/tests/ui/ReportActionItemTest.tsx @@ -145,7 +145,7 @@ describe('ReportActionItem', () => { { { { { { { { { { { { { { { { { { { { { parentReportActionID: 'parentAction', ownerAccountID: 0, }} - transactionThreadReport={undefined} + transactionThreadReportID={undefined} parentReportAction={undefined} action={action} displayAsGroup={false} @@ -1851,7 +1851,7 @@ describe('ReportActionItem', () => { { { { { { { { { { { { chatReport={undefined} report={{reportID: HARVEST_REPORT_ID}} parentReportAction={undefined} - transactionThreadReport={undefined} + transactionThreadReportID={undefined} action={action} displayAsGroup={false} shouldDisplayNewMarker={false} @@ -3021,7 +3021,7 @@ describe('ReportActionItem', () => { { beforeEach(async () => { jest.clearAllMocks(); await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${TEST_REPORT_ID}`, testReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${TEST_PARENT_REPORT_ID}`, { [TEST_ACTION_ID]: testParentReportAction, }); @@ -245,7 +246,7 @@ describe('MoneyRequestReceiptView', () => { render( @@ -267,7 +268,7 @@ describe('MoneyRequestReceiptView', () => { it('does not show action buttons when transaction has no receipt', async () => { render( - + , ); await waitForBatchedUpdatesWithAct(); @@ -284,7 +285,7 @@ describe('MoneyRequestReceiptView', () => { render( - + , ); await waitForBatchedUpdatesWithAct(); @@ -301,7 +302,7 @@ describe('MoneyRequestReceiptView', () => { render( - + , ); await waitForBatchedUpdatesWithAct(); @@ -319,7 +320,7 @@ describe('MoneyRequestReceiptView', () => { render( , @@ -338,7 +339,7 @@ describe('MoneyRequestReceiptView', () => { render( - + , ); await waitForBatchedUpdatesWithAct();