diff --git a/patches/react-native-onyx+3.0.89.patch b/patches/react-native-onyx+3.0.89.patch new file mode 100644 index 000000000000..1dc8e1b1427d --- /dev/null +++ b/patches/react-native-onyx+3.0.89.patch @@ -0,0 +1,788 @@ +diff --git a/node_modules/react-native-onyx/dist/Onyx.js b/node_modules/react-native-onyx/dist/Onyx.js +index ee33551..baee2b5 100644 +--- a/node_modules/react-native-onyx/dist/Onyx.js ++++ b/node_modules/react-native-onyx/dist/Onyx.js +@@ -209,11 +209,14 @@ function merge(key, changes) { + return mergeQueuePromise[key]; + } + mergeQueue[key] = [changes]; +- mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((existingValue) => { ++ mergeQueuePromise[key] = Promise.resolve().then(() => { + // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue + if (mergeQueue[key] == null) { + return Promise.resolve(); + } ++ // Read the existing value at merge application time (not at queue time) so that ++ // any intervening synchronous cache updates (e.g. from mergeCollection) are picked up. ++ const existingValue = OnyxUtils_1.default.get(key); + try { + const validChanges = mergeQueue[key].filter((change) => { + const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue); +@@ -292,81 +295,86 @@ function mergeCollection(collectionKey, collection) { + */ + function clear(keysToPreserve = []) { + return OnyxUtils_1.default.afterInit(() => { ++ var _a; + const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates(); + const initialKeys = Object.keys(defaultKeyStates); +- const promise = OnyxUtils_1.default.getAllKeys() +- .then((cachedKeys) => { +- var _a; +- OnyxCache_1.default.clearNullishStorageKeys(); +- const keysToBeClearedFromStorage = []; +- const keyValuesToResetIndividually = {}; +- // We need to store old and new values for collection keys to properly notify subscribers when clearing Onyx +- // because the notification process needs the old values in cache but at that point they will be already removed from it. +- const keyValuesToResetAsCollection = {}; +- const allKeys = new Set([...cachedKeys, ...initialKeys]); +- // The only keys that should not be cleared are: +- // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline +- // status, or activeClients need to remain in Onyx even when signed out) +- // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them +- // to null would cause unknown behavior) +- // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value +- for (const key of allKeys) { +- const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxKeys_1.default.isKeyMatch(preserveKey, key)); +- const isDefaultKey = key in defaultKeyStates; +- // If the key is being removed or reset to default: +- // 1. Update it in the cache +- // 2. Figure out whether it is a collection key or not, +- // since collection key subscribers need to be updated differently +- if (!isKeyToPreserve) { +- const oldValue = OnyxCache_1.default.get(key); +- const newValue = (_a = defaultKeyStates[key]) !== null && _a !== void 0 ? _a : null; +- if (newValue !== oldValue) { +- OnyxCache_1.default.set(key, newValue); +- const collectionKey = OnyxKeys_1.default.getCollectionKey(key); +- if (collectionKey) { +- if (!keyValuesToResetAsCollection[collectionKey]) { +- keyValuesToResetAsCollection[collectionKey] = { oldValues: {}, newValues: {} }; +- } +- keyValuesToResetAsCollection[collectionKey].oldValues[key] = oldValue; +- keyValuesToResetAsCollection[collectionKey].newValues[key] = newValue !== null && newValue !== void 0 ? newValue : undefined; +- } +- else { +- keyValuesToResetIndividually[key] = newValue !== null && newValue !== void 0 ? newValue : undefined; ++ const cachedKeys = OnyxUtils_1.default.getAllKeys(); ++ OnyxCache_1.default.clearNullishStorageKeys(); ++ // Clear pending merge queues so that any in-flight Onyx.merge() calls ++ // don't overwrite the default values we're about to set. ++ const mergeQueue = OnyxUtils_1.default.getMergeQueue(); ++ const mergeQueuePromise = OnyxUtils_1.default.getMergeQueuePromise(); ++ for (const key of Object.keys(mergeQueue)) { ++ delete mergeQueue[key]; ++ delete mergeQueuePromise[key]; ++ } ++ const keysToBeClearedFromStorage = []; ++ const keyValuesToResetIndividually = {}; ++ // We need to store old and new values for collection keys to properly notify subscribers when clearing Onyx ++ // because the notification process needs the old values in cache but at that point they will be already removed from it. ++ const keyValuesToResetAsCollection = {}; ++ const allKeys = new Set([...cachedKeys, ...initialKeys]); ++ // The only keys that should not be cleared are: ++ // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline ++ // status, or activeClients need to remain in Onyx even when signed out) ++ // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them ++ // to null would cause unknown behavior) ++ // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value ++ for (const key of allKeys) { ++ const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxKeys_1.default.isKeyMatch(preserveKey, key)); ++ const isDefaultKey = key in defaultKeyStates; ++ // If the key is being removed or reset to default: ++ // 1. Update it in the cache ++ // 2. Figure out whether it is a collection key or not, ++ // since collection key subscribers need to be updated differently ++ if (!isKeyToPreserve) { ++ const oldValue = OnyxCache_1.default.get(key); ++ const newValue = (_a = defaultKeyStates[key]) !== null && _a !== void 0 ? _a : null; ++ if (newValue !== oldValue) { ++ OnyxCache_1.default.set(key, newValue); ++ const collectionKey = OnyxKeys_1.default.getCollectionKey(key); ++ if (collectionKey) { ++ if (!keyValuesToResetAsCollection[collectionKey]) { ++ keyValuesToResetAsCollection[collectionKey] = { oldValues: {}, newValues: {} }; + } ++ keyValuesToResetAsCollection[collectionKey].oldValues[key] = oldValue; ++ keyValuesToResetAsCollection[collectionKey].newValues[key] = newValue !== null && newValue !== void 0 ? newValue : undefined; ++ } ++ else { ++ keyValuesToResetIndividually[key] = newValue !== null && newValue !== void 0 ? newValue : undefined; + } + } +- if (isKeyToPreserve || isDefaultKey) { +- continue; +- } +- // If it isn't preserved and doesn't have a default, we'll remove it +- keysToBeClearedFromStorage.push(key); + } +- // Exclude RAM-only keys to prevent them from being saved to storage +- const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates) +- .filter((key) => !keysToPreserve.some((preserveKey) => OnyxKeys_1.default.isKeyMatch(preserveKey, key)) && !OnyxKeys_1.default.isRamOnlyKey(key)) +- .reduce((obj, key) => { +- // eslint-disable-next-line no-param-reassign +- obj[key] = defaultKeyStates[key]; +- return obj; +- }, {})); +- // Remove only the items that we want cleared from storage, and reset others to default +- for (const key of keysToBeClearedFromStorage) +- OnyxCache_1.default.drop(key); +- return storage_1.default.removeItems(keysToBeClearedFromStorage) +- .then(() => OnyxConnectionManager_1.default.refreshSessionID()) +- .then(() => storage_1.default.multiSet(defaultKeyValuePairs)) +- .then(() => { +- DevTools_1.default.clearState(keysToPreserve); +- // Notify the subscribers for each key/value group so they can receive the new values +- for (const [key, value] of Object.entries(keyValuesToResetIndividually)) { +- OnyxUtils_1.default.keyChanged(key, value); +- } +- for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) { +- OnyxUtils_1.default.keysChanged(key, value.newValues, value.oldValues); +- } +- }); +- }) +- .then(() => undefined); ++ if (isKeyToPreserve || isDefaultKey) { ++ continue; ++ } ++ // If it isn't preserved and doesn't have a default, we'll remove it ++ keysToBeClearedFromStorage.push(key); ++ } ++ // Exclude RAM-only keys to prevent them from being saved to storage ++ const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates) ++ .filter((key) => !keysToPreserve.some((preserveKey) => OnyxKeys_1.default.isKeyMatch(preserveKey, key)) && !OnyxKeys_1.default.isRamOnlyKey(key)) ++ .reduce((obj, key) => { ++ // eslint-disable-next-line no-param-reassign ++ obj[key] = defaultKeyStates[key]; ++ return obj; ++ }, {})); ++ // Remove only the items that we want cleared from storage, and reset others to default ++ for (const key of keysToBeClearedFromStorage) ++ OnyxCache_1.default.drop(key); ++ const promise = storage_1.default.removeItems(keysToBeClearedFromStorage) ++ .then(() => OnyxConnectionManager_1.default.refreshSessionID()) ++ .then(() => storage_1.default.multiSet(defaultKeyValuePairs)) ++ .then(() => { ++ DevTools_1.default.clearState(keysToPreserve); ++ // Notify the subscribers for each key/value group so they can receive the new values ++ for (const [key, value] of Object.entries(keyValuesToResetIndividually)) { ++ OnyxUtils_1.default.keyChanged(key, value); ++ } ++ for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) { ++ OnyxUtils_1.default.keysChanged(key, value.newValues, value.oldValues); ++ } ++ }); + return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.CLEAR, promise); + }); + } +@@ -476,6 +484,11 @@ function update(data) { + mergeReplaceNullPatches: {}, + set: {}, + }); ++ // Set operations must run before merge operations so their cache writes are ++ // visible when mergeCollectionWithPatches reads previous values synchronously. ++ if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) { ++ promises.push(() => OnyxUtils_1.default.partialSetCollection({ collectionKey, collection: batchedCollectionUpdates.set })); ++ } + if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) { + promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches({ + collectionKey, +@@ -484,9 +497,6 @@ function update(data) { + isProcessingCollectionUpdate: true, + })); + } +- if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) { +- promises.push(() => OnyxUtils_1.default.partialSetCollection({ collectionKey, collection: batchedCollectionUpdates.set })); +- } + } + for (const [key, operations] of Object.entries(updateQueue)) { + if (operations[0] === null) { +diff --git a/node_modules/react-native-onyx/dist/OnyxCache.d.ts b/node_modules/react-native-onyx/dist/OnyxCache.d.ts +index e2c4c9a..fb38cf2 100644 +--- a/node_modules/react-native-onyx/dist/OnyxCache.d.ts ++++ b/node_modules/react-native-onyx/dist/OnyxCache.d.ts +@@ -119,7 +119,7 @@ declare class OnyxCache { + * @param isCollectionKeyFn - Function to determine if a key is a collection key + * @param getAllKeysFn - Function to get all keys, defaults to Storage.getAllKeys + */ +- addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Promise>): Promise; ++ addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Set): void; + /** + * Finds the least recently accessed key that can be safely evicted from storage. + * `excludeKeys` skips keys that must not be evicted (e.g. the in-flight write's own keys, +diff --git a/node_modules/react-native-onyx/dist/OnyxCache.js b/node_modules/react-native-onyx/dist/OnyxCache.js +index 102fe93..11f0a1c 100644 +--- a/node_modules/react-native-onyx/dist/OnyxCache.js ++++ b/node_modules/react-native-onyx/dist/OnyxCache.js +@@ -252,16 +252,15 @@ class OnyxCache { + * @param getAllKeysFn - Function to get all keys, defaults to Storage.getAllKeys + */ + addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn, getAllKeysFn) { +- return getAllKeysFn().then((keys) => { +- for (const evictableKey of this.evictionAllowList) { +- for (const key of keys) { +- if (!OnyxKeys_1.default.isKeyMatch(evictableKey, key)) { +- continue; +- } +- this.addLastAccessedKey(key, isCollectionKeyFn(key)); ++ const keys = getAllKeysFn(); ++ for (const evictableKey of this.evictionAllowList) { ++ for (const key of keys) { ++ if (!OnyxKeys_1.default.isKeyMatch(evictableKey, key)) { ++ continue; + } ++ this.addLastAccessedKey(key, isCollectionKeyFn(key)); + } +- }); ++ } + } + /** + * Finds the least recently accessed key that can be safely evicted from storage. +diff --git a/node_modules/react-native-onyx/dist/OnyxUtils.d.ts b/node_modules/react-native-onyx/dist/OnyxUtils.d.ts +index 91f9a52..4d91894 100644 +--- a/node_modules/react-native-onyx/dist/OnyxUtils.d.ts ++++ b/node_modules/react-native-onyx/dist/OnyxUtils.d.ts +@@ -80,17 +80,17 @@ declare function sendActionToDevTools(method: Exclude(collection: OnyxCollection, selector: Selector): Record; + /** Get some data from the store */ +-declare function get>(key: TKey): Promise; +-declare function multiGet(keys: CollectionKeyBase[]): Promise>>; ++declare function get>(key: TKey): TValue; ++declare function multiGet(keys: CollectionKeyBase[]): Map>; + /** + * This helper exists to map an array of Onyx keys such as `['report_', 'conciergeReportID']` + * to the values for those keys (correctly typed) such as `[OnyxCollection, OnyxEntry]` + * + * Note: just using `.map`, you'd end up with `Array|OnyxEntry>`, which is not what we want. This preserves the order of the keys provided. + */ +-declare function tupleGet(keys: Keys): Promise<{ ++declare function tupleGet(keys: Keys): { + [Index in keyof Keys]: OnyxValue; +-}>; ++}; + /** + * Stores a subscription ID associated with a given key. + * +@@ -105,7 +105,7 @@ declare function storeKeyBySubscriptions(key: OnyxKey, subscriptionID: number): + */ + declare function deleteKeyBySubscriptions(subscriptionID: number): void; + /** Returns current key names stored in persisted storage */ +-declare function getAllKeys(): Promise>; ++declare function getAllKeys(): Set; + /** + * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined. + * If the requested key is a collection, it will return an object with all the collection members. +diff --git a/node_modules/react-native-onyx/dist/OnyxUtils.js b/node_modules/react-native-onyx/dist/OnyxUtils.js +index 344cd49..9c718f8 100644 +--- a/node_modules/react-native-onyx/dist/OnyxUtils.js ++++ b/node_modules/react-native-onyx/dist/OnyxUtils.js +@@ -41,7 +41,7 @@ const fast_equals_1 = require("fast-equals"); + const underscore_1 = __importDefault(require("underscore")); + const DevTools_1 = __importDefault(require("./DevTools")); + const Logger = __importStar(require("./Logger")); +-const OnyxCache_1 = __importStar(require("./OnyxCache")); ++const OnyxCache_1 = __importDefault(require("./OnyxCache")); + const OnyxKeys_1 = __importDefault(require("./OnyxKeys")); + const StorageCircuitBreaker_1 = __importDefault(require("./StorageCircuitBreaker")); + const storage_1 = __importDefault(require("./storage")); +@@ -191,142 +191,19 @@ function reduceCollectionWithSelector(collection, selector) { + } + /** Get some data from the store */ + function get(key) { +- // When we already have the value in cache - resolve right away +- if (OnyxCache_1.default.hasCacheForKey(key)) { +- return Promise.resolve(OnyxCache_1.default.get(key)); +- } +- // RAM-only keys should never read from storage (they may have stale persisted data +- // from before the key was migrated to RAM-only). Mark as nullish so future get() calls +- // short-circuit via hasCacheForKey and avoid re-running this branch. +- if (OnyxKeys_1.default.isRamOnlyKey(key)) { +- OnyxCache_1.default.addNullishStorageKey(key); +- return Promise.resolve(undefined); +- } +- const taskName = `${OnyxCache_1.TASK.GET}:${key}`; +- // When a value retrieving task for this key is still running hook to it +- if (OnyxCache_1.default.hasPendingTask(taskName)) { +- return OnyxCache_1.default.getTaskPromise(taskName); +- } +- // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages +- const promise = storage_1.default.getItem(key) +- .then((val) => { +- if (skippableCollectionMemberIDs.size) { +- try { +- const [, collectionMemberID] = OnyxKeys_1.default.splitCollectionMemberKey(key); +- if (skippableCollectionMemberIDs.has(collectionMemberID)) { +- // The key is a skippable one, so we set the value to undefined. +- // eslint-disable-next-line no-param-reassign +- val = undefined; +- } +- } +- catch (e) { +- // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. +- } +- } +- // Prefer cache over stale storage if a concurrent write populated it during the read. +- const cachedValue = OnyxCache_1.default.get(key); +- if (cachedValue !== undefined) { +- return cachedValue; +- } +- if (val === undefined) { +- OnyxCache_1.default.addNullishStorageKey(key); +- return undefined; +- } +- OnyxCache_1.default.set(key, val); +- return val; +- }) +- .catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`)); +- return OnyxCache_1.default.captureTask(taskName, promise); ++ return OnyxCache_1.default.get(key); + } +-// multiGet the data first from the cache and then from the storage for the missing keys. ++// multiGet the data from the cache for all given keys. + function multiGet(keys) { +- // Keys that are not in the cache +- const missingKeys = []; +- // Tasks that are pending +- const pendingTasks = []; +- // Keys for the tasks that are pending +- const pendingKeys = []; +- // Data to be sent back to the invoker + const dataMap = new Map(); +- /** +- * We are going to iterate over all the matching keys and check if we have the data in the cache. +- * If we do then we add it to the data object. If we do not have them, then we check if there is a pending task +- * for the key. If there is such task, then we add the promise to the pendingTasks array and the key to the pendingKeys +- * array. If there is no pending task then we add the key to the missingKeys array. +- * +- * These missingKeys will be later used to multiGet the data from the storage. +- */ + for (const key of keys) { +- // RAM-only keys should never read from storage as they may have stale persisted data +- // from before the key was migrated to RAM-only. +- if (OnyxKeys_1.default.isRamOnlyKey(key)) { +- if (OnyxCache_1.default.hasCacheForKey(key)) { +- dataMap.set(key, OnyxCache_1.default.get(key)); +- } +- continue; +- } + // hasCacheForKey catches cached falsy values (0, '', false, null) as cache hits, which + // a truthy check on the value would miss. + if (OnyxCache_1.default.hasCacheForKey(key)) { + dataMap.set(key, OnyxCache_1.default.get(key)); +- continue; +- } +- const pendingKey = `${OnyxCache_1.TASK.GET}:${key}`; +- if (OnyxCache_1.default.hasPendingTask(pendingKey)) { +- pendingTasks.push(OnyxCache_1.default.getTaskPromise(pendingKey)); +- pendingKeys.push(key); +- } +- else { +- missingKeys.push(key); + } + } +- return (Promise.all(pendingTasks) +- // Wait for all the pending tasks to resolve and then add the data to the data map. +- .then((values) => { +- for (const [index, value] of values.entries()) { +- dataMap.set(pendingKeys[index], value); +- } +- return Promise.resolve(); +- }) +- // Get the missing keys using multiGet from the storage. +- .then(() => { +- if (missingKeys.length === 0) { +- return Promise.resolve(undefined); +- } +- return storage_1.default.multiGet(missingKeys); +- }) +- // Add the data from the missing keys to the data map and also merge it to the cache. +- .then((values) => { +- if (!values || values.length === 0) { +- return dataMap; +- } +- // temp object is used to merge the missing data into the cache +- const temp = {}; +- for (const [key, value] of values) { +- if (skippableCollectionMemberIDs.size) { +- try { +- const [, collectionMemberID] = OnyxKeys_1.default.splitCollectionMemberKey(key); +- if (skippableCollectionMemberIDs.has(collectionMemberID)) { +- // The key is a skippable one, so we skip this iteration. +- continue; +- } +- } +- catch (e) { +- // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. +- } +- } +- // Prefer cache over stale storage if a concurrent write populated it during +- // the read — otherwise cache.merge(temp) below would resurrect dropped fields. +- if (OnyxCache_1.default.hasCacheForKey(key)) { +- dataMap.set(key, OnyxCache_1.default.get(key)); +- continue; +- } +- dataMap.set(key, value); +- temp[key] = value; +- } +- OnyxCache_1.default.merge(temp); +- return dataMap; +- })); ++ return dataMap; + } + /** + * This helper exists to map an array of Onyx keys such as `['report_', 'conciergeReportID']` +@@ -335,7 +212,7 @@ function multiGet(keys) { + * Note: just using `.map`, you'd end up with `Array|OnyxEntry>`, which is not what we want. This preserves the order of the keys provided. + */ + function tupleGet(keys) { +- return Promise.all(keys.map((key) => get(key))); ++ return keys.map((key) => get(key)); + } + /** + * Stores a subscription ID associated with a given key. +@@ -364,25 +241,7 @@ function deleteKeyBySubscriptions(subscriptionID) { + } + /** Returns current key names stored in persisted storage */ + function getAllKeys() { +- // When we've already read stored keys, resolve right away +- const cachedKeys = OnyxCache_1.default.getAllKeys(); +- if (cachedKeys.size > 0) { +- return Promise.resolve(cachedKeys); +- } +- // When a value retrieving task for all keys is still running hook to it +- if (OnyxCache_1.default.hasPendingTask(OnyxCache_1.TASK.GET_ALL_KEYS)) { +- return OnyxCache_1.default.getTaskPromise(OnyxCache_1.TASK.GET_ALL_KEYS); +- } +- // Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages +- const promise = storage_1.default.getAllKeys().then((keys) => { +- // Filter out RAM-only keys from storage results as they may be stale entries +- // from before the key was migrated to RAM-only. +- const filteredKeys = keys.filter((key) => !OnyxKeys_1.default.isRamOnlyKey(key)); +- OnyxCache_1.default.setAllKeys(filteredKeys); +- // return the updated set of keys +- return OnyxCache_1.default.getAllKeys(); +- }); +- return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.GET_ALL_KEYS, promise); ++ return OnyxCache_1.default.getAllKeys(); + } + /** + * Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined. +@@ -622,9 +481,7 @@ function sendDataToConnection(mapping, matchedKey) { + * Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber. + */ + function getCollectionDataAndSendAsObject(matchingKeys, mapping) { +- multiGet(matchingKeys).then(() => { +- sendDataToConnection(mapping, mapping.key); +- }); ++ sendDataToConnection(mapping, mapping.key); + } + /** + * Remove a key from Onyx and update the subscribers +@@ -972,7 +829,7 @@ function subscribeToKey(connectOptions) { + return; + } + // If we are not subscribed to a collection key then there's only a single key to send an update for. +- get(mapping.key).then(() => sendDataToConnection(mapping, mapping.key)); ++ sendDataToConnection(mapping, mapping.key); + return; + } + console.error('Warning: Onyx.connect() was found without a callback'); +@@ -1244,38 +1101,37 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) { + }, {}); + } + resultCollectionKeys = Object.keys(resultCollection); +- return OnyxUtils.getAllKeys().then((persistedKeys) => { +- const mutableCollection = Object.assign({}, resultCollection); +- for (const key of persistedKeys) { +- if (!key.startsWith(collectionKey)) { +- continue; +- } +- if (resultCollectionKeys.includes(key)) { +- continue; +- } +- mutableCollection[key] = null; +- } +- const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); +- const previousCollection = OnyxUtils.getCachedCollection(collectionKey); +- for (const [key, value] of keyValuePairs) +- OnyxCache_1.default.set(key, value); +- // Skip subscriber notification on retry — already notified on attempt 0. +- // Collection-root subscribers re-fire on every keysChanged by contract. +- if (!retryAttempt) { +- keysChanged(collectionKey, mutableCollection, previousCollection); ++ const persistedKeys = OnyxUtils.getAllKeys(); ++ const mutableCollection = Object.assign({}, resultCollection); ++ for (const key of persistedKeys) { ++ if (!key.startsWith(collectionKey)) { ++ continue; + } +- // RAM-only keys are not supposed to be saved to storage +- if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) { +- OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); +- return; ++ if (resultCollectionKeys.includes(key)) { ++ continue; + } +- const inFlightKeys = new Set(keyValuePairs.map(([key]) => key)); +- return storage_1.default.multiSet(keyValuePairs) +- .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess()) +- .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt, inFlightKeys)) +- .then(() => { +- OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); +- }); ++ mutableCollection[key] = null; ++ } ++ const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); ++ const previousCollection = OnyxUtils.getCachedCollection(collectionKey); ++ for (const [key, value] of keyValuePairs) ++ OnyxCache_1.default.set(key, value); ++ // Skip subscriber notification on retry — already notified on attempt 0. ++ // Collection-root subscribers re-fire on every keysChanged by contract. ++ if (!retryAttempt) { ++ keysChanged(collectionKey, mutableCollection, previousCollection); ++ } ++ // RAM-only keys are not supposed to be saved to storage ++ if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) { ++ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); ++ return Promise.resolve(); ++ } ++ const inFlightKeys = new Set(keyValuePairs.map(([key]) => key)); ++ return storage_1.default.multiSet(keyValuePairs) ++ .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess()) ++ .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt, inFlightKeys)) ++ .then(() => { ++ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); + }); + } + /** +@@ -1319,101 +1175,85 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul + }, {}); + } + resultCollectionKeys = Object.keys(resultCollection); +- return getAllKeys() +- .then((persistedKeys) => { +- // Split to keys that exist in storage and keys that don't +- const keys = resultCollectionKeys.filter((key) => { +- if (resultCollection[key] === null) { +- remove(key, isProcessingCollectionUpdate); +- return false; +- } +- return true; +- }); +- const existingKeys = keys.filter((key) => persistedKeys.has(key)); +- const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys); +- const existingKeyCollection = existingKeys.reduce((obj, key) => { +- const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]); +- if (isEmptyArrayCoercion) { +- // Merging an object into an empty array isn't semantically correct, but we allow it +- // in case we accidentally encoded an empty object as an empty array in PHP. If you're +- // looking at a bugbot from this message, we're probably missing that key in OnyxKeys::KEYS_REQUIRING_EMPTY_OBJECT +- Logger.logAlert(`[ENSURE_BUGBOT] Onyx mergeCollection called on key "${key}" whose existing value is an empty array. Will coerce to object.`); +- } +- if (!isCompatible) { +- Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType)); +- return obj; +- } +- // eslint-disable-next-line no-param-reassign +- obj[key] = resultCollection[key]; ++ const persistedKeys = getAllKeys(); ++ // Split to keys that exist in storage and keys that don't ++ const keys = resultCollectionKeys.filter((key) => { ++ if (resultCollection[key] === null) { ++ remove(key, isProcessingCollectionUpdate); ++ return false; ++ } ++ return true; ++ }); ++ const existingKeys = keys.filter((key) => persistedKeys.has(key)); ++ const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys); ++ const existingKeyCollection = existingKeys.reduce((obj, key) => { ++ const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]); ++ if (isEmptyArrayCoercion) { ++ // Merging an object into an empty array isn't semantically correct, but we allow it ++ // in case we accidentally encoded an empty object as an empty array in PHP. If you're ++ // looking at a bugbot from this message, we're probably missing that key in OnyxKeys::KEYS_REQUIRING_EMPTY_OBJECT ++ Logger.logAlert(`[ENSURE_BUGBOT] Onyx mergeCollection called on key "${key}" whose existing value is an empty array. Will coerce to object.`); ++ } ++ if (!isCompatible) { ++ Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType)); + return obj; +- }, {}); +- const newCollection = {}; +- for (const key of keys) { +- if (persistedKeys.has(key)) { +- continue; +- } +- newCollection[key] = resultCollection[key]; + } +- // When (multi-)merging the values with the existing values in storage, +- // we don't want to remove nested null values from the data that we pass to the storage layer, +- // because the storage layer uses them to remove nested keys from storage natively. +- const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches); +- // We can safely remove nested null values when using (multi-)set, +- // because we will simply overwrite the existing values in storage. +- const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection, true); +- // finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates +- const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection); +- // Pre-warm cache for cache-miss existingKeys so cache.merge() merges the new delta into +- // the real previous storage value. Fast path (all warm) skips the pre-warm to preserve +- // promise-chain depth; slow path batches the misses into one Storage.multiGet. +- const hasColdExistingKey = existingKeys.some((key) => !OnyxCache_1.default.hasCacheForKey(key)); +- // Swallow pre-warm read failures so a transient Storage.multiGet rejection doesn't +- // skip the cache.merge() + keysChanged() below. Subscribers still see the merge even +- // when storage reads fail. +- const prewarmPromise = hasColdExistingKey +- ? multiGet(existingKeys).catch((err) => Logger.logInfo(`mergeCollectionWithPatches pre-warm failed; proceeding with cache-only merge. Error: ${err}`)) +- : Promise.resolve(); +- return prewarmPromise.then(() => { +- // Snapshot previous values from the (now-warm) cache for keysChanged's diff, then update +- // cache and notify subscribers synchronously BEFORE issuing storage writes. This matches +- // the cache-first / storage-second invariant followed by every other Onyx write method +- // (setWithRetry, applyMerge, setCollectionWithRetry, partialSetCollection, clear), +- // ensuring subscribers still reflect the merged data even if the subsequent storage +- // write fails. +- const previousCollection = getCachedCollection(collectionKey, existingKeys); +- OnyxCache_1.default.merge(finalMergedCollection); +- // Skip subscriber notification on retry — already notified on attempt 0. +- // Collection-root subscribers re-fire on every keysChanged by contract. +- if (!retryAttempt) { +- keysChanged(collectionKey, finalMergedCollection, previousCollection); +- } +- const promises = []; +- // New keys go through multiSet and existing keys through multiMerge. multiMerge on a +- // missing key stores the value just like multiSet across all backends; splitting them lets +- // multiSet strip nested nulls (the merge layer keeps them to delete nested storage keys). +- // We can skip this step for RAM-only keys as they should never be saved to storage +- if (!OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keyValuePairsForExistingCollection.length > 0) { +- promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection)); +- } +- // We can skip this step for RAM-only keys as they should never be saved to storage +- if (!OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keyValuePairsForNewCollection.length > 0) { +- promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection)); +- } +- const inFlightKeys = new Set(Object.keys(finalMergedCollection)); +- return Promise.all(promises) +- .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess()) +- .catch((error) => retryOperation(error, mergeCollectionWithPatches, { +- collectionKey, +- collection: resultCollection, +- mergeReplaceNullPatches, +- isProcessingCollectionUpdate, +- }, retryAttempt, inFlightKeys)) +- .then(() => { +- sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); +- }); +- }); +- }) +- .then(() => undefined); ++ // eslint-disable-next-line no-param-reassign ++ obj[key] = resultCollection[key]; ++ return obj; ++ }, {}); ++ const newCollection = {}; ++ for (const key of keys) { ++ if (persistedKeys.has(key)) { ++ continue; ++ } ++ newCollection[key] = resultCollection[key]; ++ } ++ // When (multi-)merging the values with the existing values in storage, ++ // we don't want to remove nested null values from the data that we pass to the storage layer, ++ // because the storage layer uses them to remove nested keys from storage natively. ++ const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches); ++ // We can safely remove nested null values when using (multi-)set, ++ // because we will simply overwrite the existing values in storage. ++ const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection, true); ++ // finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates ++ const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection); ++ // Snapshot previous values from the cache for keysChanged's diff, then update cache and notify ++ // subscribers synchronously BEFORE issuing storage writes. This matches the cache-first / ++ // storage-second invariant followed by every other Onyx write method (setWithRetry, applyMerge, ++ // setCollectionWithRetry, partialSetCollection, clear), ensuring subscribers still reflect the ++ // merged data even if the subsequent storage write fails. ++ const previousCollection = getCachedCollection(collectionKey, existingKeys); ++ OnyxCache_1.default.merge(finalMergedCollection); ++ // Skip subscriber notification on retry — already notified on attempt 0. ++ // Collection-root subscribers re-fire on every keysChanged by contract. ++ if (!retryAttempt) { ++ keysChanged(collectionKey, finalMergedCollection, previousCollection); ++ } ++ const promises = []; ++ // New keys go through multiSet and existing keys through multiMerge. multiMerge on a ++ // missing key stores the value just like multiSet across all backends; splitting them lets ++ // multiSet strip nested nulls (the merge layer keeps them to delete nested storage keys). ++ // We can skip this step for RAM-only keys as they should never be saved to storage ++ if (!OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keyValuePairsForExistingCollection.length > 0) { ++ promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection)); ++ } ++ // We can skip this step for RAM-only keys as they should never be saved to storage ++ if (!OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keyValuePairsForNewCollection.length > 0) { ++ promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection)); ++ } ++ const inFlightKeys = new Set(Object.keys(finalMergedCollection)); ++ return Promise.all(promises) ++ .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess()) ++ .catch((error) => retryOperation(error, mergeCollectionWithPatches, { ++ collectionKey, ++ collection: resultCollection, ++ mergeReplaceNullPatches, ++ isProcessingCollectionUpdate, ++ }, retryAttempt, inFlightKeys)) ++ .then(() => { ++ sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection); ++ }); + } + /** + * Sets keys in a collection by replacing all targeted collection members with new values. +@@ -1450,29 +1290,28 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) { + }, {}); + } + resultCollectionKeys = Object.keys(resultCollection); +- return getAllKeys().then((persistedKeys) => { +- const mutableCollection = Object.assign({}, resultCollection); +- const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key)); +- const previousCollection = getCachedCollection(collectionKey, existingKeys); +- const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); +- for (const [key, value] of keyValuePairs) +- OnyxCache_1.default.set(key, value); +- // Skip subscriber notification on retry — already notified on attempt 0. +- // Collection-root subscribers re-fire on every keysChanged by contract. +- if (!retryAttempt) { +- keysChanged(collectionKey, mutableCollection, previousCollection); +- } +- if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) { +- sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); +- return; +- } +- const inFlightKeys = new Set(keyValuePairs.map(([key]) => key)); +- return storage_1.default.multiSet(keyValuePairs) +- .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess()) +- .catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt, inFlightKeys)) +- .then(() => { +- sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); +- }); ++ const persistedKeys = getAllKeys(); ++ const mutableCollection = Object.assign({}, resultCollection); ++ const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key)); ++ const previousCollection = getCachedCollection(collectionKey, existingKeys); ++ const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true); ++ for (const [key, value] of keyValuePairs) ++ OnyxCache_1.default.set(key, value); ++ // Skip subscriber notification on retry — already notified on attempt 0. ++ // Collection-root subscribers re-fire on every keysChanged by contract. ++ if (!retryAttempt) { ++ keysChanged(collectionKey, mutableCollection, previousCollection); ++ } ++ if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) { ++ sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); ++ return Promise.resolve(); ++ } ++ const inFlightKeys = new Set(keyValuePairs.map(([key]) => key)); ++ return storage_1.default.multiSet(keyValuePairs) ++ .then(() => StorageCircuitBreaker_1.default.recordWriteSuccess()) ++ .catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt, inFlightKeys)) ++ .then(() => { ++ sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); + }); + } + function logKeyChanged(onyxMethod, key, value, hasChanged) { diff --git a/src/components/GPSTripStateChecker/index.native.tsx b/src/components/GPSTripStateChecker/index.native.tsx index a17f7eae10a6..10c8612fc316 100644 --- a/src/components/GPSTripStateChecker/index.native.tsx +++ b/src/components/GPSTripStateChecker/index.native.tsx @@ -39,7 +39,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) { const isBackgroundTaskRunning = await hasStartedLocationUpdatesAsync(BACKGROUND_LOCATION_TRACKING_TASK_NAME); diff --git a/src/components/GPSTripStateChecker/useUpdateGpsTripOnReconnect.ts b/src/components/GPSTripStateChecker/useUpdateGpsTripOnReconnect.ts index eb45654966d6..2332f1fc3f67 100644 --- a/src/components/GPSTripStateChecker/useUpdateGpsTripOnReconnect.ts +++ b/src/components/GPSTripStateChecker/useUpdateGpsTripOnReconnect.ts @@ -42,8 +42,7 @@ function useUpdateGpsTripOnReconnect({gpsPoints}: {gpsPoints: GPSPoint[][]}) { const waypointAddresses = (await Promise.all(waypointUpdates)).filter((waypoints) => !!waypoints.point.address); // To avoid race conditions, we need to get the latest gpsDraftDetails, because reverse geocoding may even take a few seconds - const gpsDraftDetailsPromiseResult = await OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS).catch(() => undefined); - const latestGpsDraftDetails = gpsDraftDetailsPromiseResult; + const latestGpsDraftDetails = OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS) ?? undefined; const latestGpsPoints = getGpsPoints(latestGpsDraftDetails) ?? gpsPoints; const newGpsPoints = [...latestGpsPoints]; 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 ) { - 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; } diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 75f14ca1331f..c3094be5bfb4 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -7,6 +7,7 @@ import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import ReceiptAudit, {ReceiptAuditMessages} from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import ReceiptHoverZoom from '@components/ReceiptHoverZoom'; +import {useSearchResultsContext} from '@components/Search/SearchContext'; import Tooltip from '@components/Tooltip'; import useActiveRoute from '@hooks/useActiveRoute'; @@ -88,14 +89,17 @@ import {conciergePersonalDetailSelector, personalDetailsSelector} from '@selecto import mapValues from 'lodash/mapValues'; import React, {useEffect, useMemo, useRef, 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 HoveredDistanceEReceipt from './HoveredDistanceEReceipt'; import {isElementHovered, resetButtonHoverState} from './receiptHoverUtils'; import ReportActionItemImage from './ReportActionItemImage'; type MoneyRequestReceiptViewProps = { - /** The report currently being looked at */ - report: OnyxEntry; + /** 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/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/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/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}; 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/libs/actions/OnyxDerived/index.ts b/src/libs/actions/OnyxDerived/index.ts index 0ce1169a8b68..dcd3c9726681 100644 --- a/src/libs/actions/OnyxDerived/index.ts +++ b/src/libs/actions/OnyxDerived/index.ts @@ -37,117 +37,115 @@ 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}`); - } - }; - - // 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); - } - - // 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; - - 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}, - }); + 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, + }; + + 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; + } - 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]; - - if (OnyxKeys.isCollectionKey(dependencyOnyxKey)) { - Onyx.connectWithoutView({ - key: dependencyOnyxKey, - 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); - }, - }); - } + 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}, + }); + + 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]; + + if (OnyxKeys.isCollectionKey(dependencyOnyxKey)) { + Onyx.connectWithoutView({ + key: dependencyOnyxKey, + 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/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/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} 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/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/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/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/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..5fcf98ee0576 100644 --- a/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts +++ b/src/pages/inbox/report/ReportActionCompose/useEditMessage.ts @@ -1,11 +1,10 @@ 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'; 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'; @@ -18,11 +17,13 @@ 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'; + +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 */ @@ -36,14 +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 [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${actionOwnerReportID}`); - const isOriginalReportArchived = useReportIsArchived(actionOwnerReportID); const {stopEditing, submitEdit} = useReportActionActiveEditActions(); @@ -67,6 +64,7 @@ function useEditMessage({reportID, originalReportID, reportAction, shouldScrollT * the new content. */ function publishDraft(draftMessage: string) { + console.log('publishDraft', draftMessage); if (!reportAction) { return; } @@ -78,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 +87,9 @@ 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/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; diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 8cd3388621fc..286cf7f7566f 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'; @@ -105,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; @@ -166,7 +176,10 @@ type ReportActionItemProps = { function ReportActionItem({ action, report, - transactionThreadReport, + transactionThreadReportID, + transactionThreadPolicyID, + transactionThreadParentReportActionID, + transactionThreadParentReportID, chatReport, linkedReportActionID, displayAsGroup, @@ -187,7 +200,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 +268,8 @@ 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; } @@ -430,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 { // 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; } diff --git a/src/pages/inbox/report/ReportActionItemParentAction.tsx b/src/pages/inbox/report/ReportActionItemParentAction.tsx index b91f754849cd..842dbbdb3c04 100644 --- a/src/pages/inbox/report/ReportActionItemParentAction.tsx +++ b/src/pages/inbox/report/ReportActionItemParentAction.tsx @@ -7,6 +7,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useThemeStyles from '@hooks/useThemeStyles'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {shouldExcludeAncestorReportAction} from '@libs/ReportUtils'; @@ -40,8 +41,8 @@ type ReportActionItemParentActionProps = { /** The current report is displayed */ report: OnyxEntry; - /** 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/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} 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/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)} /> 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/src/setup/backgroundLocationTrackingTask/index.native.ts b/src/setup/backgroundLocationTrackingTask/index.native.ts index b082f4d680b1..ad3876fc3971 100644 --- a/src/setup/backgroundLocationTrackingTask/index.native.ts +++ b/src/setup/backgroundLocationTrackingTask/index.native.ts @@ -23,8 +23,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) ?? undefined; + const netInfoState = await NetInfo.fetch(); if (!gpsDraftDetails) { return; } @@ -58,8 +58,7 @@ async function updateStartAddress(gpsPoints: GPSPoint[][], isOffline: boolean) { const address = await addressFromGpsPoint({lat: startPoint.lat, long: startPoint.long}); // To avoid race conditions, we need to get the latest gpsDraftDetails, because reverse geocoding may even take a few seconds - const gpsDraftDetailsPromiseResult = await OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS).catch(() => undefined); - const updatedGpsDraftDetails = gpsDraftDetailsPromiseResult ?? undefined; + const updatedGpsDraftDetails = OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS) ?? undefined; const updatedGpsPoints = updatedGpsDraftDetails ? getGpsPoints(updatedGpsDraftDetails) : gpsPoints; if (address !== null) { diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts index 74b510be5a73..6724dba81baa 100644 --- a/tests/actions/PolicyCategoryTest.ts +++ b/tests/actions/PolicyCategoryTest.ts @@ -436,7 +436,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); @@ -496,7 +496,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); @@ -551,7 +551,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); @@ -598,7 +598,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 1d0d67c1d4e1..1a52b58d7915 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -229,7 +229,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); @@ -239,7 +239,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); }); @@ -272,7 +272,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(); @@ -318,7 +318,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); @@ -329,7 +329,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(); @@ -375,7 +375,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(); }); @@ -414,7 +414,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) { @@ -429,7 +429,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) { @@ -482,7 +482,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); @@ -493,7 +493,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(); @@ -685,7 +685,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(); @@ -697,7 +697,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(); @@ -745,7 +745,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(); @@ -787,7 +787,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); }); @@ -842,7 +842,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); @@ -886,7 +886,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); @@ -898,7 +898,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(); @@ -925,7 +925,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); @@ -971,7 +971,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'); @@ -2294,7 +2294,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); @@ -2304,7 +2304,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(); @@ -2335,7 +2335,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); @@ -2372,7 +2372,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(); @@ -2407,7 +2407,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 @@ -2468,12 +2468,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); @@ -2523,12 +2523,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); @@ -2579,12 +2579,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); @@ -2636,12 +2636,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 aa4730628cc8..8f978605faeb 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -2353,7 +2353,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); @@ -2399,7 +2399,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); @@ -2599,7 +2599,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/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', () => { { 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, @@ -608,7 +608,7 @@ describe('Unread Indicators', () => { // Navigate to the chat and simulate leaving a comment from the current user .then(() => navigateToSidebarOptionWithoutAct(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, @@ -641,7 +641,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, undefined, ''); } @@ -665,7 +665,7 @@ describe('Unread Indicators', () => { await signInAndGetAppWithUnreadChat(); await navigateToSidebarOptionWithoutAct(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, @@ -868,7 +868,7 @@ describe('Unread Indicators', () => { await signInAndGetAppWithUnreadChat(); await navigateToSidebarOptionWithoutAct(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({ @@ -897,7 +897,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 90e94332551f..2d8291e25e3a 100644 --- a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx +++ b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx @@ -506,7 +506,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(); @@ -565,7 +565,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; @@ -633,7 +633,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); @@ -673,7 +673,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'); @@ -780,7 +780,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(); @@ -883,7 +883,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; @@ -941,7 +941,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/ui/components/MoneyRequestReceiptViewTest.tsx b/tests/ui/components/MoneyRequestReceiptViewTest.tsx index 6286bb13843a..efeda1868376 100644 --- a/tests/ui/components/MoneyRequestReceiptViewTest.tsx +++ b/tests/ui/components/MoneyRequestReceiptViewTest.tsx @@ -224,6 +224,7 @@ describe('MoneyRequestReceiptView', () => { 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(); diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index d114984ce348..bfbded64ec7d 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -61,7 +61,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: {}, }); @@ -71,7 +71,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: { @@ -86,7 +86,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: { @@ -98,7 +98,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: {}, @@ -109,7 +109,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', @@ -165,7 +165,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 @@ -184,7 +184,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); @@ -207,7 +207,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, { @@ -219,7 +219,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); @@ -269,7 +269,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({}); }); @@ -302,7 +302,7 @@ describe('OnyxDerived', () => { await waitForBatchedUpdates(); - const derivedReportAttributes = await OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); + const derivedReportAttributes = OnyxUtils.get(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); await waitForBatchedUpdates(); @@ -354,7 +354,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', @@ -399,7 +399,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', @@ -430,7 +430,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 --- @@ -440,7 +440,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(); }); }); @@ -455,7 +455,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({}); }); @@ -473,7 +473,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}), @@ -495,7 +495,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}); @@ -512,7 +512,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}), @@ -526,7 +526,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}), @@ -545,7 +545,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}), @@ -571,7 +571,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}), @@ -589,7 +589,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}), @@ -603,7 +603,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}), @@ -622,7 +622,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}), diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index 18a66f009783..894665421421 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -113,7 +113,7 @@ describe('PersistedRequests', () => { await waitForBatchedUpdates(); expect(PersistedRequests.getOngoingRequest()).toEqual(newRequest); - expect((await OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS)) == null).toBe(true); + expect(OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS) == null).toBe(true); } finally { global.File = originalFile; } @@ -156,7 +156,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'})); }); })); @@ -190,7 +190,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 @@ -227,7 +227,7 @@ describe('PersistedRequests persistence guarantees', () => { expect(nextRequest).toEqual(requestWithFile); expect(PersistedRequests.getOngoingRequest()).toEqual(requestWithFile); - expect((await OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS)) == null).toBe(true); + expect(OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS) == null).toBe(true); } finally { global.File = originalFile; } diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index ccfddf3285a4..35bd296a3838 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -1913,8 +1913,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'); @@ -1932,7 +1932,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); }); @@ -1948,7 +1948,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); }); @@ -1981,7 +1981,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(); @@ -2128,12 +2128,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); @@ -2143,10 +2143,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); @@ -2173,12 +2173,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); }); @@ -2199,7 +2199,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([]); @@ -2238,7 +2238,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); @@ -2298,10 +2298,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); @@ -2312,10 +2312,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); @@ -2368,10 +2368,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); }); @@ -2482,7 +2482,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(); @@ -2544,7 +2544,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 8809239f2323..43502b08019c 100644 --- a/tests/unit/WorkspacesSettingsUtilsTest.ts +++ b/tests/unit/WorkspacesSettingsUtilsTest.ts @@ -51,7 +51,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); @@ -73,7 +73,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); @@ -102,7 +102,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); @@ -124,7 +124,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); @@ -152,7 +152,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); @@ -174,7 +174,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 77ae864adc61..96a77d51c39a 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -108,7 +108,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, @@ -122,7 +122,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, @@ -266,7 +266,7 @@ describe('canEditFieldOfMoneyRequest', () => { }, }); 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({ @@ -293,7 +293,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({ @@ -326,7 +326,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({ @@ -346,7 +346,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({ 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},