From 6ab6a13fba69b8e87eae7d5c0ca1f39881578b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 25 Apr 2024 15:57:46 -0600 Subject: [PATCH 001/512] Display submitted message in report action --- src/pages/home/report/ReportActionItem.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index efb2d8ba73fb..cde76434f382 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -39,6 +39,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation'; @@ -580,6 +581,9 @@ function ReportActionItem({ } else if (ReportActionsUtils.isOldDotReportAction(action)) { // This handles all historical actions from OldDot that we just want to display the message text children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(report.total), report.currency); + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD_COMMENT) { From 8388c28b4d7c999b644efba595cfe3468a190e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 25 Apr 2024 16:11:57 -0600 Subject: [PATCH 002/512] Fix ts error --- src/pages/home/report/ReportActionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index cde76434f382..d3c0430ba02e 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -582,7 +582,7 @@ function ReportActionItem({ // This handles all historical actions from OldDot that we just want to display the message text children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { - const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(report.total), report.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(report?.total ?? 0), report.currency); children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { children = ; From 39d7f11c826e9a43295970c2ddbf857d0b161195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 1 May 2024 15:25:54 -0600 Subject: [PATCH 003/512] Set SUBMITTED message to clipboard --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 013bc484fc63..8e0eddf7ceb6 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -30,6 +30,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type {Beta, ReportAction, ReportActionReactions, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; @@ -360,6 +361,10 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(mentionWhisperMessage); } else if (ReportActionsUtils.isActionableTrackExpense(reportAction)) { setClipboardMessage('What would you like to do with this expense?'); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) { + const expenseReport = ReportUtils.getReport(reportID); + const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(expenseReport?.total ?? 0), expenseReport?.currency); + setClipboardMessage(Localize.translateLocal('iou.submittedAmount', {formattedAmount})); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { Clipboard.setString(Localize.translateLocal('iou.heldExpense')); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { From e7bb1533279afe058e97793d06a51087c7989e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 1 May 2024 15:34:45 -0600 Subject: [PATCH 004/512] Fix linting --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 8e0eddf7ceb6..5021e888ea84 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -12,6 +12,7 @@ import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactio import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import Clipboard from '@libs/Clipboard'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import EmailUtils from '@libs/EmailUtils'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; @@ -30,7 +31,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type {Beta, ReportAction, ReportActionReactions, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; From c88356f40cb6b0f162078be03c400f72abf7386a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 9 May 2024 23:43:34 -0400 Subject: [PATCH 005/512] Client previousReportActionID proof of concept --- src/CONST.ts | 2 + src/libs/API/index.ts | 2 + src/libs/Middleware/Pagination.ts | 51 + src/libs/Middleware/index.ts | 3 +- src/libs/ReportActionsUtils.ts | 74 +- tests/unit/ReportActionsUtilsTest.ts | 1383 +++++++++++++++++++++++++- 6 files changed, 1462 insertions(+), 53 deletions(-) create mode 100644 src/libs/Middleware/Pagination.ts diff --git a/src/CONST.ts b/src/CONST.ts index 6517ece4276d..de6c1658ed7c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4777,6 +4777,8 @@ const CONST = { REFERRER: { NOTIFICATION: 'notification', }, + + PAGINATION_GAP_ID: '-1', } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index bfa1b95836f8..26b6813100a6 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -28,6 +28,8 @@ Request.use(Middleware.Reauthentication); // If an optimistic ID is not used by the server, this will update the remaining serialized requests using that optimistic ID to use the correct ID instead. Request.use(Middleware.HandleUnusedOptimisticID); +Request.use(Middleware.Pagination); + // SaveResponseInOnyx - Merges either the successData or failureData (or finallyData, if included in place of the former two values) into Onyx depending on if the call was successful or not. This needs to be the LAST middleware we use, don't add any // middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state. Request.use(Middleware.SaveResponseInOnyx); diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts new file mode 100644 index 000000000000..47cfdef25a43 --- /dev/null +++ b/src/libs/Middleware/Pagination.ts @@ -0,0 +1,51 @@ +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction, ReportActions, Response} from '@src/types/onyx'; +import type Middleware from './types'; + +// eslint-disable-next-line rulesdir/no-inline-named-export +export function setPreviousReportActionID(reportActions: ReportActions, sortedReportActions: ReportAction[], insertGap: boolean): void { + for (let i = 0; i < sortedReportActions.length; i++) { + const previousReportActionID = sortedReportActions[i + 1]?.reportActionID; + // eslint-disable-next-line no-param-reassign + reportActions[sortedReportActions[i].reportActionID].previousReportActionID = previousReportActionID ?? (insertGap ? CONST.PAGINATION_GAP_ID : undefined); + } +} + +function getReportActions(response: Response | void, reportID: string) { + return response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions | undefined; +} + +const Pagination: Middleware = (requestResponse, request) => { + if (request.command === WRITE_COMMANDS.OPEN_REPORT || request.command === READ_COMMANDS.GET_OLDER_ACTIONS || request.command === READ_COMMANDS.GET_NEWER_ACTIONS) { + return requestResponse.then((response = {}) => { + const reportID = request.data?.reportID as string | undefined; + if (!reportID) { + return Promise.resolve(response); + } + + const reportActions = getReportActions(response, reportID); + if (!reportActions) { + return Promise.resolve(response); + } + + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true); + + setPreviousReportActionID( + reportActions, + sortedReportActions, + // For GetNewerActions we do not want to insert a gap since we attach + // at the start of the list. + request.command !== READ_COMMANDS.GET_NEWER_ACTIONS, + ); + + return Promise.resolve(response); + }); + } + + return requestResponse; +}; + +export default Pagination; diff --git a/src/libs/Middleware/index.ts b/src/libs/Middleware/index.ts index 3b1790b3cda5..6fbc3a43fdce 100644 --- a/src/libs/Middleware/index.ts +++ b/src/libs/Middleware/index.ts @@ -1,7 +1,8 @@ import HandleUnusedOptimisticID from './HandleUnusedOptimisticID'; import Logging from './Logging'; +import Pagination from './Pagination'; import Reauthentication from './Reauthentication'; import RecheckConnection from './RecheckConnection'; import SaveResponseInOnyx from './SaveResponseInOnyx'; -export {HandleUnusedOptimisticID, Logging, Reauthentication, RecheckConnection, SaveResponseInOnyx}; +export {HandleUnusedOptimisticID, Logging, Reauthentication, RecheckConnection, SaveResponseInOnyx, Pagination}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9f6ad81e46b1..7c4d47e29430 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -282,29 +282,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } -function isOptimisticAction(reportAction: ReportAction) { - return ( - !!reportAction.isOptimisticAction || - reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || - reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE - ); -} - -function shouldIgnoreGap(currentReportAction: ReportAction | undefined, nextReportAction: ReportAction | undefined) { - if (!currentReportAction || !nextReportAction) { - return false; - } - return ( - isOptimisticAction(currentReportAction) || - isOptimisticAction(nextReportAction) || - !!getWhisperedTo(currentReportAction).length || - !!getWhisperedTo(nextReportAction).length || - currentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM || - nextReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || - nextReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED - ); -} - /** * Returns a sorted and filtered list of report actions from a report and it's associated child * transaction thread report in order to correctly display reportActions from both reports in the one-transaction report view. @@ -326,6 +303,38 @@ function getCombinedReportActions(reportActions: ReportAction[], transactionThre return getSortedReportActions(filteredReportActions, true); } +function hasNextContinuousAction(sortedReportActions: ReportAction[], index: number): boolean { + for (let i = index; i < sortedReportActions.length - 1; i++) { + // If we hit a gap marker, the action is in the gap and not continuous. + if (sortedReportActions[i].previousReportActionID === CONST.PAGINATION_GAP_ID) { + return false; + } + // If we hit an action with a previousReportActionID, the action is not in a gap and is continuous. + if (sortedReportActions[i].previousReportActionID !== undefined) { + return true; + } + } + + // If we reach the end the action is in a gap and not continuous. + return false; +} + +function hasPreviousContinuousAction(sortedReportActions: ReportAction[], index: number): boolean { + for (let i = index; i >= 0; i--) { + // If we hit a gap marker, the action is in the gap and not continuous. + if (sortedReportActions[i].previousReportActionID === CONST.PAGINATION_GAP_ID) { + return false; + } + // If we hit an action with a previousReportActionID, the action is not in a gap and is continuous. + if (sortedReportActions[i].previousReportActionID !== undefined) { + return true; + } + } + + // If we reach the start the action is not in a gap and is continuous. + return true; +} + /** * Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. * See unit tests for example of inputs and expected outputs. @@ -336,8 +345,12 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? if (id) { index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); + // If we are linking to an action with no previousReportActionID in a gap just return it. + if (index >= 0 && sortedReportActions[index].previousReportActionID === undefined && !hasPreviousContinuousAction(sortedReportActions, index)) { + return [sortedReportActions[index]]; + } } else { - index = sortedReportActions.findIndex((reportAction) => !isOptimisticAction(reportAction)); + index = sortedReportActions.findIndex((reportAction) => reportAction.previousReportActionID !== undefined); } if (index === -1) { @@ -351,19 +364,20 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? // Iterate forwards through the array, starting from endIndex. i.e: newer to older // This loop checks the continuity of actions by comparing the current item's previousReportActionID with the next item's reportActionID. - // It ignores optimistic actions, whispers and InviteToRoom actions while ( - (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) || - shouldIgnoreGap(sortedReportActions[endIndex], sortedReportActions[endIndex + 1]) + endIndex < sortedReportActions.length - 1 && + sortedReportActions[endIndex].previousReportActionID !== CONST.PAGINATION_GAP_ID && + (sortedReportActions[endIndex].previousReportActionID !== undefined || hasNextContinuousAction(sortedReportActions, endIndex)) ) { endIndex++; } // Iterate backwards through the sortedReportActions, starting from startIndex. (older to newer) - // This loop ensuress continuity in a sequence of actions by comparing the current item's reportActionID with the previous item's previousReportActionID. + // This loop ensures continuity in a sequence of actions by comparing the current item's reportActionID with the previous item's previousReportActionID. while ( - (startIndex > 0 && sortedReportActions[startIndex].reportActionID === sortedReportActions[startIndex - 1].previousReportActionID) || - shouldIgnoreGap(sortedReportActions[startIndex], sortedReportActions[startIndex - 1]) + startIndex > 0 && + sortedReportActions[startIndex - 1].previousReportActionID !== CONST.PAGINATION_GAP_ID && + (sortedReportActions[startIndex - 1].previousReportActionID !== undefined || hasPreviousContinuousAction(sortedReportActions, startIndex - 1)) ) { startIndex--; } diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 19b4a23e9028..b9b00e808e55 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -448,7 +448,7 @@ describe('ReportActionsUtils', () => { // Given these sortedReportActions { reportActionID: '1', - previousReportActionID: undefined, + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -569,7 +569,7 @@ describe('ReportActionsUtils', () => { // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) { reportActionID: '9', - previousReportActionID: '8', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -639,7 +639,7 @@ describe('ReportActionsUtils', () => { // Note: another gap { reportActionID: '14', - previousReportActionID: '13', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -710,7 +710,7 @@ describe('ReportActionsUtils', () => { const expectedResult = [ { reportActionID: '1', - previousReportActionID: undefined, + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -839,7 +839,7 @@ describe('ReportActionsUtils', () => { // Given these sortedReportActions { reportActionID: '1', - previousReportActionID: undefined, + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -960,7 +960,7 @@ describe('ReportActionsUtils', () => { // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) { reportActionID: '9', - previousReportActionID: '8', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1030,7 +1030,7 @@ describe('ReportActionsUtils', () => { // Note: another gap { reportActionID: '14', - previousReportActionID: '13', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1101,7 +1101,7 @@ describe('ReportActionsUtils', () => { const expectedResult = [ { reportActionID: '9', - previousReportActionID: '8', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1179,7 +1179,7 @@ describe('ReportActionsUtils', () => { // Given these sortedReportActions { reportActionID: '1', - previousReportActionID: undefined, + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1300,7 +1300,7 @@ describe('ReportActionsUtils', () => { // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) { reportActionID: '9', - previousReportActionID: '8', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1370,7 +1370,7 @@ describe('ReportActionsUtils', () => { // Note: another gap { reportActionID: '14', - previousReportActionID: '13', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1441,7 +1441,7 @@ describe('ReportActionsUtils', () => { const expectedResult = [ { reportActionID: '14', - previousReportActionID: '13', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1519,7 +1519,7 @@ describe('ReportActionsUtils', () => { // Given these sortedReportActions { reportActionID: '1', - previousReportActionID: undefined, + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1640,7 +1640,7 @@ describe('ReportActionsUtils', () => { // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) { reportActionID: '9', - previousReportActionID: '8', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1710,7 +1710,7 @@ describe('ReportActionsUtils', () => { // Note: another gap { reportActionID: '14', - previousReportActionID: '13', + previousReportActionID: CONST.PAGINATION_GAP_ID, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1808,7 +1808,7 @@ describe('ReportActionsUtils', () => { }, { reportActionID: '2', - previousReportActionID: '1', + previousReportActionID: undefined, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1826,7 +1826,7 @@ describe('ReportActionsUtils', () => { }, { reportActionID: '3', - previousReportActionID: '2', + previousReportActionID: undefined, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1844,7 +1844,7 @@ describe('ReportActionsUtils', () => { }, { reportActionID: '4', - previousReportActionID: '3', + previousReportActionID: undefined, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1862,7 +1862,7 @@ describe('ReportActionsUtils', () => { }, { reportActionID: '5', - previousReportActionID: '4', + previousReportActionID: undefined, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1880,7 +1880,7 @@ describe('ReportActionsUtils', () => { }, { reportActionID: '6', - previousReportActionID: '5', + previousReportActionID: undefined, created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1898,7 +1898,127 @@ describe('ReportActionsUtils', () => { }, { reportActionID: '7', - previousReportActionID: '6', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ]; + + const expectedResult = [...input]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), ''); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given actions with no previousReportActionID, they will be included if they are not in a gap', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + { + reportActionID: '1', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + // Node: these 2 actions were inserted without a previousReportActionID. + { + reportActionID: '5', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '6', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '4', + previousReportActionID: '3', created: '2022-11-13 22:27:01.825', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { @@ -1916,11 +2036,1230 @@ describe('ReportActionsUtils', () => { }, ]; - const expectedResult = input; + const expectedResult = [...input]; // Reversing the input array to simulate descending order sorting as per our data structure const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), ''); expect(result).toStrictEqual(expectedResult.reverse()); }); + + it('given actions with no previousReportActionID and an input ID of 9, ..., 12 they will not be included if they are in a gap', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + { + reportActionID: '1', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + // Note: these 2 actions were inserted without a previousReportActionID. + { + reportActionID: '18', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '19', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + + // Note: there's a gap here + { + reportActionID: '9', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + // Note: these 2 actions were inserted without a previousReportActionID. + { + reportActionID: '20', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '21', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + // Note: another gap + { + reportActionID: '14', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + + const expectedResult = [ + { + reportActionID: '9', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '10'); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given actions with no previousReportActionID at the start and no input ID, they will be included in the result', () => { + const input = [ + // Given these sortedReportActions + { + reportActionID: '14', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '18', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '19', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + + const expectedResult = [ + { + reportActionID: '14', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '18', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '19', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given actions with no previousReportActionID at the end and no input ID, they will not be included in the result', () => { + const input = [ + // Given these sortedReportActions + { + reportActionID: '18', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '19', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '14', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + + const expectedResult = [ + { + reportActionID: '14', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given the input id of an action without previousReportActionID in a gap, it will return only that action', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + { + reportActionID: '1', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + // Note: these 2 actions were inserted without a previousReportActionID. + { + reportActionID: '18', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '19', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + + // Note: there's a gap here + { + reportActionID: '9', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + // Note: another gap + { + reportActionID: '14', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + + const expectedResult = [ + { + reportActionID: '19', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '19'); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given the input id of an action without previousReportActionID not in a gap, it will return the page it is in', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + { + reportActionID: '1', + previousReportActionID: CONST.PAGINATION_GAP_ID, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + // Node: these 2 actions were inserted without a previousReportActionID. + { + reportActionID: '5', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '6', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '7', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ]; + + const expectedResult = [...input]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '6'); + expect(result).toStrictEqual(expectedResult.reverse()); + }); }); describe('getLastVisibleAction', () => { From 8f39e392ab80f6d982c825414f83bc140e96edf3 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 23 May 2024 23:41:06 -0400 Subject: [PATCH 006/512] New implementation --- src/CONST.ts | 2 - src/libs/Middleware/Pagination.ts | 61 +- src/libs/ReportActionsUtils.ts | 137 +- src/libs/migrateOnyx.ts | 2 - .../CheckForPreviousReportActionID.ts | 65 - src/pages/home/ReportScreen.tsx | 4 +- src/pages/home/report/ReportActionsView.tsx | 1 + src/types/onyx/ReportAction.ts | 3 - src/types/onyx/ReportMetadata.ts | 12 +- src/types/onyx/index.ts | 2 +- tests/ui/UnreadIndicatorsTest.tsx | 18 +- tests/unit/MigrationTest.ts | 229 +- tests/unit/ReportActionsUtilsTest.ts | 3200 +++-------------- tests/utils/LHNTestUtils.tsx | 2 - tests/utils/ReportTestUtils.ts | 2 - tests/utils/TestHelper.ts | 3 +- tests/utils/collections/reportActions.ts | 1 - 17 files changed, 573 insertions(+), 3171 deletions(-) delete mode 100644 src/libs/migrations/CheckForPreviousReportActionID.ts diff --git a/src/CONST.ts b/src/CONST.ts index de6c1658ed7c..6517ece4276d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4777,8 +4777,6 @@ const CONST = { REFERRER: { NOTIFICATION: 'notification', }, - - PAGINATION_GAP_ID: '-1', } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 47cfdef25a43..c20175b2a68a 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -1,45 +1,60 @@ +// TODO: Is this a legit use case for exposing `OnyxCache`, or should we use `Onyx.connect`? +import OnyxCache from 'react-native-onyx/dist/OnyxCache'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportAction, ReportActions, Response} from '@src/types/onyx'; +import type {ReportActions, ReportMetadata, Response} from '@src/types/onyx'; import type Middleware from './types'; -// eslint-disable-next-line rulesdir/no-inline-named-export -export function setPreviousReportActionID(reportActions: ReportActions, sortedReportActions: ReportAction[], insertGap: boolean): void { - for (let i = 0; i < sortedReportActions.length; i++) { - const previousReportActionID = sortedReportActions[i + 1]?.reportActionID; - // eslint-disable-next-line no-param-reassign - reportActions[sortedReportActions[i].reportActionID].previousReportActionID = previousReportActionID ?? (insertGap ? CONST.PAGINATION_GAP_ID : undefined); - } -} - -function getReportActions(response: Response | void, reportID: string) { +function getReportActions(response: Response, reportID: string) { return response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions | undefined; } const Pagination: Middleware = (requestResponse, request) => { if (request.command === WRITE_COMMANDS.OPEN_REPORT || request.command === READ_COMMANDS.GET_OLDER_ACTIONS || request.command === READ_COMMANDS.GET_NEWER_ACTIONS) { - return requestResponse.then((response = {}) => { + return requestResponse.then((response) => { + if (!response?.onyxData) { + return Promise.resolve(response); + } + const reportID = request.data?.reportID as string | undefined; if (!reportID) { + // TODO: Should not happen, should we throw? return Promise.resolve(response); } - const reportActions = getReportActions(response, reportID); - if (!reportActions) { + const reportActionID = request.data?.reportActionID as string | undefined; + + // Create a new page based on the response actions. + const pageReportActions = getReportActions(response, reportID); + const pageSortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(pageReportActions, true); + if (pageSortedReportActions.length === 0) { + // Must have at least 1 action to create a page. return Promise.resolve(response); } + const newPage = { + // Use null to indicate this is the first page. + firstReportActionID: reportActionID == null ? null : pageSortedReportActions.at(0)?.reportActionID ?? null, + // TODO: It would be nice to have a way to know if this is the last page. + lastReportActionID: pageSortedReportActions.at(-1)?.reportActionID ?? null, + }; + + const reportActions = OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`) as ReportActions | undefined; + // TODO: Do we need to do proper merge here or this is ok? + const allReportActions = {...reportActions, ...pageReportActions}; + const allSortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true); + const reportMetadata = OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`) as ReportMetadata | undefined; + const pages = reportMetadata?.pages ?? []; + const newPages = ReportActionsUtils.mergeContinuousPages(allSortedReportActions, [...pages, newPage]); - setPreviousReportActionID( - reportActions, - sortedReportActions, - // For GetNewerActions we do not want to insert a gap since we attach - // at the start of the list. - request.command !== READ_COMMANDS.GET_NEWER_ACTIONS, - ); + response.onyxData.push({ + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + onyxMethod: 'merge', + value: { + pages: newPages, + }, + }); return Promise.resolve(response); }); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7c4d47e29430..b3dee3d9f589 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -20,8 +20,9 @@ import type { OriginalMessageReimbursementDequeued, } from '@src/types/onyx/OriginalMessage'; import type Report from '@src/types/onyx/Report'; -import type {Message, ReportActionBase, ReportActionMessageJSON, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; +import type {Message, ReportActionBase, ReportActionMessageJSON, ReportActions} from '@src/types/onyx/ReportAction'; +import type {ReportMetadataPage} from '@src/types/onyx/ReportMetadata'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DateUtils from './DateUtils'; @@ -303,86 +304,101 @@ function getCombinedReportActions(reportActions: ReportAction[], transactionThre return getSortedReportActions(filteredReportActions, true); } -function hasNextContinuousAction(sortedReportActions: ReportAction[], index: number): boolean { - for (let i = index; i < sortedReportActions.length - 1; i++) { - // If we hit a gap marker, the action is in the gap and not continuous. - if (sortedReportActions[i].previousReportActionID === CONST.PAGINATION_GAP_ID) { - return false; - } - // If we hit an action with a previousReportActionID, the action is not in a gap and is continuous. - if (sortedReportActions[i].previousReportActionID !== undefined) { - return true; - } - } +type ReportMetadataPageWithIndexes = ReportMetadataPage & { + firstReportActionIndex: number; + lastReportActionIndex: number; +}; - // If we reach the end the action is in a gap and not continuous. - return false; +function getPagesWithIndexes(sortedReportActions: ReportAction[], pages: ReportMetadataPage[]): ReportMetadataPageWithIndexes[] { + return pages.map((page) => ({ + ...page, + // TODO: It should be possible to make this O(n) by starting the search at the previous found index. + // TODO: What if reportActionID is not in the list? Could happen if an action is deleted from another device. + firstReportActionIndex: page.firstReportActionID == null ? 0 : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === page.firstReportActionID), + lastReportActionIndex: + page.lastReportActionID == null ? sortedReportActions.length - 1 : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === page.lastReportActionID), + })); } -function hasPreviousContinuousAction(sortedReportActions: ReportAction[], index: number): boolean { - for (let i = index; i >= 0; i--) { - // If we hit a gap marker, the action is in the gap and not continuous. - if (sortedReportActions[i].previousReportActionID === CONST.PAGINATION_GAP_ID) { - return false; +function mergeContinuousPages(sortedReportActions: ReportAction[], pages: ReportMetadataPage[]): ReportMetadataPage[] { + const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); + + // Pages need to be sorted by firstReportActionIndex ascending then by lastReportActionIndex descending. + const sortedPages = pagesWithIndexes.sort((a, b) => { + if (a.firstReportActionIndex !== b.firstReportActionIndex) { + return a.firstReportActionIndex - b.firstReportActionIndex; } - // If we hit an action with a previousReportActionID, the action is not in a gap and is continuous. - if (sortedReportActions[i].previousReportActionID !== undefined) { - return true; + return b.lastReportActionIndex - a.lastReportActionIndex; + }); + + const result = [sortedPages[0]]; + for (let i = 1; i < sortedPages.length; i++) { + const page = sortedPages[i]; + const prevPage = sortedPages[i - 1]; + + // Current page in inside the previous page, skip. + if (page.lastReportActionIndex <= prevPage.lastReportActionIndex) { + // eslint-disable-next-line no-continue + continue; } + + // Current page is continuous with the previous page, merge. + // This happens if the ids from the current page and previous page are the same + // or if the indexes overlap. + if (page.firstReportActionID === prevPage.lastReportActionID || page.firstReportActionIndex < prevPage.lastReportActionIndex) { + result[result.length - 1] = { + firstReportActionID: prevPage.firstReportActionID, + firstReportActionIndex: prevPage.firstReportActionIndex, + lastReportActionID: page.lastReportActionID, + lastReportActionIndex: page.lastReportActionIndex, + }; + // eslint-disable-next-line no-continue + continue; + } + + // No overlap, add the current page as is. + result.push(page); } - // If we reach the start the action is not in a gap and is continuous. - return true; + // Remove extraneous props. + return result.map((page) => ({firstReportActionID: page.firstReportActionID, lastReportActionID: page.lastReportActionID})); } /** - * Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. + * Returns the page of actions that contains the given reportActionID, or the first page if null. * See unit tests for example of inputs and expected outputs. * Note: sortedReportActions sorted in descending order */ -function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { - let index; +function getContinuousReportActionChain(sortedReportActions: ReportAction[], pages: ReportMetadataPage[], id?: string): ReportAction[] { + if (pages.length === 0) { + return id ? [] : sortedReportActions; + } + + const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); + + let page: ReportMetadataPageWithIndexes; if (id) { - index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); - // If we are linking to an action with no previousReportActionID in a gap just return it. - if (index >= 0 && sortedReportActions[index].previousReportActionID === undefined && !hasPreviousContinuousAction(sortedReportActions, index)) { - return [sortedReportActions[index]]; + const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); + + // If we are linking to an action that doesn't exist in onyx, return an empty array. + if (index === -1) { + return []; } - } else { - index = sortedReportActions.findIndex((reportAction) => reportAction.previousReportActionID !== undefined); - } - if (index === -1) { - // if no non-pending action is found, that means all actions on the report are optimistic - // in this case, we'll assume the whole chain of reportActions is continuous and return it in its entirety - return id ? [] : sortedReportActions; - } + const linkedPage = pagesWithIndexes.find((pageIndex) => index >= pageIndex.firstReportActionIndex && index <= pageIndex.lastReportActionIndex); - let startIndex = index; - let endIndex = index; - - // Iterate forwards through the array, starting from endIndex. i.e: newer to older - // This loop checks the continuity of actions by comparing the current item's previousReportActionID with the next item's reportActionID. - while ( - endIndex < sortedReportActions.length - 1 && - sortedReportActions[endIndex].previousReportActionID !== CONST.PAGINATION_GAP_ID && - (sortedReportActions[endIndex].previousReportActionID !== undefined || hasNextContinuousAction(sortedReportActions, endIndex)) - ) { - endIndex++; - } + // If we are linking to an action in a gap just return it. + if (!linkedPage) { + return [sortedReportActions[index]]; + } - // Iterate backwards through the sortedReportActions, starting from startIndex. (older to newer) - // This loop ensures continuity in a sequence of actions by comparing the current item's reportActionID with the previous item's previousReportActionID. - while ( - startIndex > 0 && - sortedReportActions[startIndex - 1].previousReportActionID !== CONST.PAGINATION_GAP_ID && - (sortedReportActions[startIndex - 1].previousReportActionID !== undefined || hasPreviousContinuousAction(sortedReportActions, startIndex - 1)) - ) { - startIndex--; + page = linkedPage; + } else { + page = pagesWithIndexes[0]; } - return sortedReportActions.slice(startIndex, endIndex + 1); + return sortedReportActions.slice(page.firstReportActionIndex, page.lastReportActionIndex + 1); } /** @@ -1279,6 +1295,7 @@ export { isActionableJoinRequestPending, isActionableTrackExpense, isLinkedTransactionHeld, + mergeContinuousPages, }; export type {LastVisibleMessage}; diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 3556746dca2f..332e4a020cab 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -1,5 +1,4 @@ import Log from './Log'; -import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; import NVPMigration from './migrations/NVPMigration'; import Participants from './migrations/Participants'; @@ -17,7 +16,6 @@ export default function () { // Add all migrations to an array so they are executed in order const migrationPromises = [ RenameCardIsVirtual, - CheckForPreviousReportActionID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, diff --git a/src/libs/migrations/CheckForPreviousReportActionID.ts b/src/libs/migrations/CheckForPreviousReportActionID.ts deleted file mode 100644 index 7e4bbe9ffb3e..000000000000 --- a/src/libs/migrations/CheckForPreviousReportActionID.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type {OnyxCollection} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import Log from '@libs/Log'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; - -function getReportActionsFromOnyx(): Promise> { - return new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - return resolve(allReportActions); - }, - }); - }); -} - -/** - * This migration checks for the 'previousReportActionID' key in the first valid reportAction of a report in Onyx. - * If the key is not found then all reportActions for all reports are removed from Onyx. - */ -export default function (): Promise { - return getReportActionsFromOnyx().then((allReportActions) => { - if (Object.keys(allReportActions ?? {}).length === 0) { - Log.info(`[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no reportActions`); - return; - } - - let firstValidValue: OnyxTypes.ReportAction | undefined; - - Object.values(allReportActions ?? {}).some((reportActions) => - Object.values(reportActions ?? {}).some((reportActionData) => { - if ('reportActionID' in reportActionData) { - firstValidValue = reportActionData; - return true; - } - - return false; - }), - ); - - if (!firstValidValue) { - Log.info(`[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no valid reportActions`); - return; - } - - if (firstValidValue.previousReportActionID) { - Log.info(`[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete`); - return; - } - - // If previousReportActionID not found: - Log.info(`[Migrate Onyx] CheckForPreviousReportActionID Migration: removing all reportActions because previousReportActionID not found in the first valid reportAction`); - - const onyxData: OnyxCollection = {}; - - Object.keys(allReportActions ?? {}).forEach((onyxKey) => { - onyxData[onyxKey] = {}; - }); - - return Onyx.multiSet(onyxData as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, Record>); - }); -} diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index f1a5bbe10231..17c2d3d76507 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -270,8 +270,8 @@ function ReportScreen({ if (!sortedAllReportActions.length) { return []; } - return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute); - }, [reportActionIDFromRoute, sortedAllReportActions]); + return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportMetadata?.pages ?? [], reportActionIDFromRoute); + }, [reportActionIDFromRoute, sortedAllReportActions, reportMetadata?.pages]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. // If we have cached reportActions, they will be shown immediately. diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index d1e45f3998cd..deab9a2fbc61 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -422,6 +422,7 @@ function ReportActionsView({ }, [hasCachedActionOnFirstRender]); useEffect(() => { + // TODO: Can this be deleted now? // Temporary solution for handling REPORT_PREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP // This code should be removed once REPORT_PREVIEW is no longer repositioned. // We need to call openReport for gaps created by moving REPORT_PREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index d7333feb8650..9af1e60c73e2 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -128,9 +128,6 @@ type ReportActionBase = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** @deprecated Used in old report actions before migration. Replaced by reportActionID. */ sequenceNumber?: number; - /** The ID of the previous reportAction on the report. It is a string represenation of a 64-bit integer (or null for CREATED actions). */ - previousReportActionID?: string; - actorAccountID?: number; /** The account of the last message's actor */ diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts index 1c6035853564..cb62abf39170 100644 --- a/src/types/onyx/ReportMetadata.ts +++ b/src/types/onyx/ReportMetadata.ts @@ -1,3 +1,10 @@ +type ReportMetadataPage = { + /** The first report action ID in the page. Null indicates that it is the first page. */ + firstReportActionID: string | null; + /** The last report action ID in the page. Null indicates that it is the last page. */ + lastReportActionID: string | null; +}; + type ReportMetadata = { /** Are we loading newer report actions? */ isLoadingNewerReportActions?: boolean; @@ -16,6 +23,9 @@ type ReportMetadata = { /** The time when user last visited the report */ lastVisitTime?: string; + + /** Pagination info */ + pages?: ReportMetadataPage[]; }; -export default ReportMetadata; +export type {ReportMetadata, ReportMetadataPage}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index eb3ef2eefc8d..4c18582a7bf9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -57,7 +57,7 @@ import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; import type ReportActionsDrafts from './ReportActionsDrafts'; -import type ReportMetadata from './ReportMetadata'; +import type {ReportMetadata} from './ReportMetadata'; import type ReportNameValuePairs from './ReportNameValuePairs'; import type ReportNextStep from './ReportNextStep'; import type ReportUserIsTyping from './ReportUserIsTyping'; diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index e5c7e0359eed..e156fe68222b 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -251,15 +251,15 @@ function signInAndGetAppWithUnreadChat(): Promise { }, ], }, - 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), - 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), - 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), - 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), - 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), - 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), - 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), - 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), - 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), + 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1'), + 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2'), + 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3'), + 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4'), + 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5'), + 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6'), + 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7'), + 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8'), + 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9'), }); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index 9f0a3433932c..06fd9984fd2d 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -1,15 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import CONST from '@src/CONST'; +import Onyx from 'react-native-onyx'; import Log from '@src/libs/Log'; -import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; -import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; -import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@src/libs/getPlatform'); @@ -30,229 +26,6 @@ describe('Migrations', () => { return waitForBatchedUpdates(); }); - describe('CheckForPreviousReportActionID', () => { - it("Should work even if there's no reportAction data in Onyx", () => - CheckForPreviousReportActionID().then(() => - expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no reportActions'), - )); - - it('Should remove all report actions given that a previousReportActionID does not exist', () => { - const reportActionsCollectionDataSet = toCollectionDataSet( - ONYXKEYS.COLLECTION.REPORT_ACTIONS, - [ - { - 1: { - reportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '1', - }, - 2: {reportActionID: '2', created: '', actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, reportID: '1'}, - }, - ], - (item) => item[1].reportID ?? '', - ); - - return Onyx.multiSet(reportActionsCollectionDataSet) - .then(CheckForPreviousReportActionID) - .then(() => { - expect(LogSpy).toHaveBeenCalledWith( - '[Migrate Onyx] CheckForPreviousReportActionID Migration: removing all reportActions because previousReportActionID not found in the first valid reportAction', - ); - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - const expectedReportAction = {}; - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); - }, - }); - }); - }); - - it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => { - const reportActionsCollectionDataSet = toCollectionDataSet( - ONYXKEYS.COLLECTION.REPORT_ACTIONS, - [ - { - 1: { - reportActionID: '1', - previousReportActionID: '0', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '1', - }, - 2: { - reportActionID: '2', - previousReportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '1', - }, - }, - ], - (item) => item[1].reportID ?? '', - ); - - return Onyx.multiSet(reportActionsCollectionDataSet) - .then(CheckForPreviousReportActionID) - .then(() => { - expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - const expectedReportAction = { - 1: { - reportActionID: '1', - previousReportActionID: '0', - }, - 2: { - reportActionID: '2', - previousReportActionID: '1', - }, - }; - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); - }, - }); - }); - }); - - it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => { - const reportActionsCollectionDataSet = toCollectionDataSet( - ONYXKEYS.COLLECTION.REPORT_ACTIONS, - [ - { - 1: { - reportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '4', - }, - 2: { - reportActionID: '2', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '4', - }, - }, - ], - (item) => item[1].reportID ?? '', - ); - - return Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, - ...reportActionsCollectionDataSet, - }) - .then(CheckForPreviousReportActionID) - .then(() => { - expect(LogSpy).toHaveBeenCalledWith( - '[Migrate Onyx] CheckForPreviousReportActionID Migration: removing all reportActions because previousReportActionID not found in the first valid reportAction', - ); - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - const expectedReportAction = {}; - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); - }, - }); - }); - }); - - it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => { - const reportActionsCollectionDataSet = toCollectionDataSet( - ONYXKEYS.COLLECTION.REPORT_ACTIONS, - [ - { - 1: { - reportActionID: '1', - previousReportActionID: '10', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '4', - }, - 2: { - reportActionID: '2', - previousReportActionID: '23', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED, - reportID: '4', - }, - }, - ], - (item) => item[1].reportID ?? '', - ); - - return Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, - ...reportActionsCollectionDataSet, - }) - .then(CheckForPreviousReportActionID) - .then(() => { - expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - const expectedReportAction1 = {}; - const expectedReportAction4 = { - 1: { - reportActionID: '1', - previousReportActionID: '10', - }, - 2: { - reportActionID: '2', - previousReportActionID: '23', - }, - }; - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction1); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); - }, - }); - }); - }); - - it('Should skip if no valid reportActions', () => { - const setQueries: CollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: null, - }; - return Onyx.multiSet(setQueries) - .then(CheckForPreviousReportActionID) - .then(() => { - expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no valid reportActions'); - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - const expectedReportAction = {}; - Onyx.disconnect(connectionID); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); - expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); - }, - }); - }); - }); - }); - describe('KeyReportActionsDraftByReportActionID', () => { it("Should work even if there's no reportActionsDrafts data in Onyx", () => KeyReportActionsDraftByReportActionID().then(() => diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index b9b00e808e55..ba7398e768a6 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import type {KeyValueMapping} from 'react-native-onyx'; +import type {ReportMetadataPage} from '@src/types/onyx/ReportMetadata'; import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; @@ -8,6 +9,26 @@ import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; +function createReportAction(id: string) { + return { + reportActionID: id, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + describe('ReportActionsUtils', () => { beforeAll(() => Onyx.init({ @@ -446,2819 +467,462 @@ describe('ReportActionsUtils', () => { it('given an input ID of 1, ..., 7 it will return the report actions with id 1 - 7', () => { const input: ReportAction[] = [ // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + // Gap + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + // Gap + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = [ + {firstReportActionID: '17', lastReportActionID: '14'}, + {firstReportActionID: '12', lastReportActionID: '9'}, + {firstReportActionID: '7', lastReportActionID: '1'}, + ]; + + const expectedResult = [ + // Expect these sortedReportActions + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '3'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + // Gap + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + // Gap + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = [ + {firstReportActionID: '17', lastReportActionID: '14'}, + {firstReportActionID: '12', lastReportActionID: '9'}, + {firstReportActionID: '7', lastReportActionID: '1'}, + ]; + + const expectedResult = [ + // Expect these sortedReportActions + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '10'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + // Gap + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + // Gap + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = [ + {firstReportActionID: '17', lastReportActionID: '14'}, + {firstReportActionID: '12', lastReportActionID: '9'}, + {firstReportActionID: '7', lastReportActionID: '1'}, + ]; + + const expectedResult = [ + // Expect these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '16'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an input ID of 8 or 13 which do not exist in Onyx it will return an empty array', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + // Gap + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + // Gap + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = [ + {firstReportActionID: '17', lastReportActionID: '14'}, + {firstReportActionID: '12', lastReportActionID: '9'}, + {firstReportActionID: '7', lastReportActionID: '1'}, + ]; + + // Expect these sortedReportActions + const expectedResult: ReportAction[] = []; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '8'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an input ID of an action in a gap it will return only that action', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + createReportAction('13'), + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + createReportAction('8'), + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = [ + {firstReportActionID: '17', lastReportActionID: '14'}, + {firstReportActionID: '12', lastReportActionID: '9'}, + {firstReportActionID: '7', lastReportActionID: '1'}, + ]; + + const expectedResult: ReportAction[] = [ + // Expect these sortedReportActions + createReportAction('8'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '8'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an empty input ID and the report only contains pending actions, it will return all actions', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = []; + + // Expect these sortedReportActions + const expectedResult = [...input]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, ''); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an empty input ID and the report only contains pending actions, it will return an empty array', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = []; + + // Expect these sortedReportActions + const expectedResult: ReportAction[] = []; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '4'); + expect(result).toStrictEqual(expectedResult); + }); + + it('does not include actions outside of pages', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + createReportAction('13'), + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + createReportAction('8'), + createReportAction('7'), + createReportAction('6'), + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + + const pages: ReportMetadataPage[] = [ + {firstReportActionID: '17', lastReportActionID: '14'}, + {firstReportActionID: '12', lastReportActionID: '9'}, + {firstReportActionID: '7', lastReportActionID: '2'}, + ]; + + const expectedResult = [ + // Expect these sortedReportActions + createReportAction('12'), + createReportAction('11'), + createReportAction('10'), + createReportAction('9'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '10'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given a page with null firstReportActionID include actions from the start', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + ]; + + const pages: ReportMetadataPage[] = [{firstReportActionID: null, lastReportActionID: '14'}]; + + const expectedResult = [ + // Expect these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, ''); + expect(result).toStrictEqual(expectedResult); + }); + + it('given a page with null lastReportActionID include actions to the end', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + ]; + + const pages: ReportMetadataPage[] = [{firstReportActionID: '17', lastReportActionID: null}]; + + const expectedResult = [ + // Expect these sortedReportActions + createReportAction('17'), + createReportAction('16'), + createReportAction('15'), + createReportAction('14'), + ]; + const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, ''); + expect(result).toStrictEqual(expectedResult); + }); + }); + + describe('mergeContinuousPages', () => { + it('merges continuous pages', () => { + const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; + const pages: ReportMetadataPage[] = [ { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '3', }, { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '3', + lastReportActionID: '1', }, + ]; + const expectedResult = [ { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '1', }, + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('merges overlapping pages', () => { + const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; + const pages: ReportMetadataPage[] = [ { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '4', + lastReportActionID: '2', }, { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '3', + lastReportActionID: '1', }, + ]; + const expectedResult = [ { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '4', + lastReportActionID: '1', }, + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); - // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + it('merges included pages', () => { + const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; + const pages: ReportMetadataPage[] = [ { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '1', }, { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '2', }, + ]; + const expectedResult = [ { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '1', }, + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('do not merge separate pages', () => { + const sortedReportActions = [ + createReportAction('5'), + createReportAction('4'), + // Gap + createReportAction('2'), + createReportAction('1'), + ]; + const pages: ReportMetadataPage[] = [ { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '4', }, - - // Note: another gap { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult = [ - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '3'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: another gap - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult = [ - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '10'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { - const input = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: another gap - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult = [ - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given an input ID of 8 or 13 which are not exist in Onyx it will return an empty array', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: another gap - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult: ReportAction[] = []; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '8'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given an empty input ID and the report only contains pending actions, it will return all actions', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '2', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '3', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '4', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '5', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '6', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '7', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - ]; - - const expectedResult = [...input]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), ''); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given actions with no previousReportActionID, they will be included if they are not in a gap', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - // Node: these 2 actions were inserted without a previousReportActionID. - { - reportActionID: '5', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '6', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - ]; - - const expectedResult = [...input]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), ''); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given actions with no previousReportActionID and an input ID of 9, ..., 12 they will not be included if they are in a gap', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - // Note: these 2 actions were inserted without a previousReportActionID. - { - reportActionID: '18', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '19', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: there's a gap here - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - // Note: these 2 actions were inserted without a previousReportActionID. - { - reportActionID: '20', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '21', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - // Note: another gap - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult = [ - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '10'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given actions with no previousReportActionID at the start and no input ID, they will be included in the result', () => { - const input = [ - // Given these sortedReportActions - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '18', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '19', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult = [ - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '18', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '19', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given actions with no previousReportActionID at the end and no input ID, they will not be included in the result', () => { - const input = [ - // Given these sortedReportActions - { - reportActionID: '18', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '19', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - - const expectedResult = [ - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '15', - previousReportActionID: '14', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '16', - previousReportActionID: '15', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '17', - previousReportActionID: '16', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given the input id of an action without previousReportActionID in a gap, it will return only that action', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '5', - previousReportActionID: '4', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '6', - previousReportActionID: '5', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '7', - previousReportActionID: '6', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - // Note: these 2 actions were inserted without a previousReportActionID. - { - reportActionID: '18', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '19', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - - // Note: there's a gap here - { - reportActionID: '9', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '10', - previousReportActionID: '9', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '11', - previousReportActionID: '10', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - reportActionID: '12', - previousReportActionID: '11', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - // Note: another gap - { - reportActionID: '14', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '2', + lastReportActionID: '1', }, ]; - const expectedResult = [ { - reportActionID: '19', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], + firstReportActionID: '5', + lastReportActionID: '4', + }, + { + firstReportActionID: '2', + lastReportActionID: '1', }, ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '19'); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); }); - it('given the input id of an action without previousReportActionID not in a gap, it will return the page it is in', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions + it('sorts pages', () => { + const sortedReportActions = [ + createReportAction('9'), + createReportAction('8'), + // Gap + createReportAction('6'), + createReportAction('5'), + // Gap + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; + const pages: ReportMetadataPage[] = [ { - reportActionID: '1', - previousReportActionID: CONST.PAGINATION_GAP_ID, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '3', + lastReportActionID: '1', }, { - reportActionID: '2', - previousReportActionID: '1', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '3', + lastReportActionID: '2', }, - // Node: these 2 actions were inserted without a previousReportActionID. { - reportActionID: '5', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '6', + lastReportActionID: '5', }, { - reportActionID: '6', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '17', + lastReportActionID: '8', }, + ]; + const expectedResult = [ { - reportActionID: '7', - previousReportActionID: undefined, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '17', + lastReportActionID: '8', }, { - reportActionID: '3', - previousReportActionID: '2', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '6', + lastReportActionID: '5', }, { - reportActionID: '4', - previousReportActionID: '3', - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + firstReportActionID: '3', + lastReportActionID: '1', }, ]; - - const expectedResult = [...input]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '6'); - expect(result).toStrictEqual(expectedResult.reverse()); + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); }); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index d35eb61feb35..01674a9068c5 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -139,14 +139,12 @@ function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0 function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = 0): ReportAction { const timestamp = Date.now() - millisecondsInThePast; const created = DateUtils.getDBTime(timestamp); - const previousReportActionID = lastFakeReportActionID; const reportActionID = ++lastFakeReportActionID; return { actor, actorAccountID: 1, reportActionID: `${reportActionID}`, - previousReportActionID: `${previousReportActionID}`, actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, shouldShow: true, created, diff --git a/tests/utils/ReportTestUtils.ts b/tests/utils/ReportTestUtils.ts index b9dc50aecec0..e0e75b5b1695 100644 --- a/tests/utils/ReportTestUtils.ts +++ b/tests/utils/ReportTestUtils.ts @@ -41,7 +41,6 @@ const getFakeReportAction = (index: number, actionName?: ActionName): ReportActi }, ], reportActionID: index.toString(), - previousReportActionID: (index === 0 ? 0 : index - 1).toString(), reportActionTimestamp: 1696243169753, sequenceNumber: 0, shouldShow: true, @@ -64,7 +63,6 @@ const getMockedReportActionsMap = (length = 100): ReportActions => { originalMessage: { linkedReportID: reportID.toString(), }, - previousReportActionID: index.toString(), } as ReportAction; return {[reportID]: reportAction}; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index bd107ba6ed56..9cac63e73ad1 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -200,7 +200,7 @@ function setPersonalDetails(login: string, accountID: number) { return waitForBatchedUpdates(); } -function buildTestReportComment(created: string, actorAccountID: number, actionID: string | null = null, previousReportActionID: string | null = null) { +function buildTestReportComment(created: string, actorAccountID: number, actionID: string | null = null) { const reportActionID = actionID ?? NumberUtils.rand64().toString(); return { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, @@ -209,7 +209,6 @@ function buildTestReportComment(created: string, actorAccountID: number, actionI message: [{type: 'COMMENT', html: `Comment ${actionID}`, text: `Comment ${actionID}`}], reportActionID, actorAccountID, - previousReportActionID, }; } diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index 65cbb3ba966e..c3d5c5671e06 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -33,7 +33,6 @@ export default function createRandomReportAction(index: number): ReportAction { // eslint-disable-next-line @typescript-eslint/no-explicit-any actionName: rand(flattenActionNamesValues(CONST.REPORT.ACTIONS.TYPE)) as any, reportActionID: index.toString(), - previousReportActionID: (index === 0 ? 0 : index - 1).toString(), actorAccountID: index, person: [ { From 273ec0fb6014c7ccac266789cca252ee8a86af0e Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 24 May 2024 17:09:10 -0400 Subject: [PATCH 007/512] wip --- src/CONST.ts | 2 + src/ONYXKEYS.ts | 2 + src/libs/Middleware/Pagination.ts | 22 +-- src/types/onyx/ReportActionsPages.ts | 3 + src/types/onyx/ReportMetadata.ts | 12 +- src/types/onyx/index.ts | 4 +- tests/ui/PaginationTest.tsx | 237 +++++++++++++++++++++++++++ tests/unit/ReportActionsUtilsTest.ts | 61 +++---- tests/utils/TestHelper.ts | 41 ++++- 9 files changed, 322 insertions(+), 62 deletions(-) create mode 100644 src/types/onyx/ReportActionsPages.ts create mode 100644 tests/ui/PaginationTest.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 6517ece4276d..e404813b06db 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4777,6 +4777,8 @@ const CONST = { REFERRER: { NOTIFICATION: 'notification', }, + + PAGINATION_START_ID: '-1', } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ddf37fba2354..f8bfb1edf698 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -347,6 +347,7 @@ const ONYXKEYS = { REPORT_METADATA: 'reportMetadata_', REPORT_ACTIONS: 'reportActions_', REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', + REPORT_ACTIONS_PAGES: 'reportActionsPages_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', @@ -557,6 +558,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.ReportActionsPages; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index c20175b2a68a..99033a043a40 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -2,8 +2,9 @@ import OnyxCache from 'react-native-onyx/dist/OnyxCache'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActions, ReportMetadata, Response} from '@src/types/onyx'; +import type {ReportActions, ReportActionsPages, Response} from '@src/types/onyx'; import type Middleware from './types'; function getReportActions(response: Response, reportID: string) { @@ -32,28 +33,23 @@ const Pagination: Middleware = (requestResponse, request) => { // Must have at least 1 action to create a page. return Promise.resolve(response); } - const newPage = { - // Use null to indicate this is the first page. - firstReportActionID: reportActionID == null ? null : pageSortedReportActions.at(0)?.reportActionID ?? null, - // TODO: It would be nice to have a way to know if this is the last page. - lastReportActionID: pageSortedReportActions.at(-1)?.reportActionID ?? null, - }; + const newPage = pageSortedReportActions.map((action) => action.reportActionID); + if (reportActionID == null) { + newPage.unshift(CONST.PAGINATION_START_ID); + } const reportActions = OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`) as ReportActions | undefined; // TODO: Do we need to do proper merge here or this is ok? const allReportActions = {...reportActions, ...pageReportActions}; const allSortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - const reportMetadata = OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`) as ReportMetadata | undefined; - const pages = reportMetadata?.pages ?? []; + const pages = (OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`) ?? []) as ReportActionsPages; const newPages = ReportActionsUtils.mergeContinuousPages(allSortedReportActions, [...pages, newPage]); response.onyxData.push({ - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, onyxMethod: 'merge', - value: { - pages: newPages, - }, + value: newPages, }); return Promise.resolve(response); diff --git a/src/types/onyx/ReportActionsPages.ts b/src/types/onyx/ReportActionsPages.ts new file mode 100644 index 000000000000..0737bd69c695 --- /dev/null +++ b/src/types/onyx/ReportActionsPages.ts @@ -0,0 +1,3 @@ +type ReportActionsPages = string[][]; + +export default ReportActionsPages; diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts index cb62abf39170..1c6035853564 100644 --- a/src/types/onyx/ReportMetadata.ts +++ b/src/types/onyx/ReportMetadata.ts @@ -1,10 +1,3 @@ -type ReportMetadataPage = { - /** The first report action ID in the page. Null indicates that it is the first page. */ - firstReportActionID: string | null; - /** The last report action ID in the page. Null indicates that it is the last page. */ - lastReportActionID: string | null; -}; - type ReportMetadata = { /** Are we loading newer report actions? */ isLoadingNewerReportActions?: boolean; @@ -23,9 +16,6 @@ type ReportMetadata = { /** The time when user last visited the report */ lastVisitTime?: string; - - /** Pagination info */ - pages?: ReportMetadataPage[]; }; -export type {ReportMetadata, ReportMetadataPage}; +export default ReportMetadata; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 4c18582a7bf9..8accfca8b16e 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -57,7 +57,8 @@ import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; import type ReportActionsDrafts from './ReportActionsDrafts'; -import type {ReportMetadata} from './ReportMetadata'; +import type ReportActionsPages from './ReportActionsPages'; +import type ReportMetadata from './ReportMetadata'; import type ReportNameValuePairs from './ReportNameValuePairs'; import type ReportNextStep from './ReportNextStep'; import type ReportUserIsTyping from './ReportUserIsTyping'; @@ -141,6 +142,7 @@ export type { ReportActions, ReportActionsDraft, ReportActionsDrafts, + ReportActionsPages, ReportMetadata, ReportNextStep, Request, diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx new file mode 100644 index 000000000000..638025adfa70 --- /dev/null +++ b/tests/ui/PaginationTest.tsx @@ -0,0 +1,237 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type * as NativeNavigation from '@react-navigation/native'; +import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; +import {addSeconds, format, subMinutes, subSeconds} from 'date-fns'; +import {utcToZonedTime} from 'date-fns-tz'; +import React from 'react'; +import {AppState, DeviceEventEmitter, Linking} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type Animated from 'react-native-reanimated'; +import * as CollectionUtils from '@libs/CollectionUtils'; +import DateUtils from '@libs/DateUtils'; +import * as Localize from '@libs/Localize'; +import LocalNotification from '@libs/Notification/LocalNotification'; +import * as NumberUtils from '@libs/NumberUtils'; +import * as Pusher from '@libs/Pusher/pusher'; +import PusherConnectionManager from '@libs/PusherConnectionManager'; +import FontUtils from '@styles/utils/FontUtils'; +import * as AppActions from '@userActions/App'; +import * as Report from '@userActions/Report'; +import * as User from '@userActions/User'; +import App from '@src/App'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import appSetup from '@src/setup'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; +import PusherHelper from '../utils/PusherHelper'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +// We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App +jest.setTimeout(30000); + +jest.mock('../../src/libs/Notification/LocalNotification'); +jest.mock('../../src/components/Icon/Expensicons'); +jest.mock('../../src/components/ConfirmedRoute.tsx'); + +// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest +jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ + __esModule: true, + default: { + ignoreLogs: jest.fn(), + ignoreAllLogs: jest.fn(), + }, +})); + +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + createAnimatedPropAdapter: jest.fn, + useReducedMotion: jest.fn, +})); + +/** + * We need to keep track of the transitionEnd callback so we can trigger it in our tests + */ +let transitionEndCB: () => void; + +type ListenerMock = { + triggerTransitionEnd: () => void; + addListener: jest.Mock; +}; + +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. + * + * @returns An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = (): ListenerMock => { + const transitionEndListeners: Array<() => void> = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + const {triggerTransitionEnd, addListener} = createAddListenerMock(); + transitionEndCB = triggerTransitionEnd; + + const useNavigation = () => + ({ + navigate: jest.fn(), + ...actualNav.useNavigation, + getState: () => ({ + routes: [], + }), + addListener, + } as typeof NativeNavigation.useNavigation); + + return { + ...actualNav, + useNavigation, + getState: () => ({ + routes: [], + }), + } as typeof NativeNavigation; +}); + +const fetchMock = TestHelper.getGlobalFetchMock(); + +beforeAll(() => { + global.fetch = fetchMock; + + Linking.setInitialURL('https://new.expensify.com/'); + appSetup(); + + // Connect to Pusher + PusherConnectionManager.init(); + Pusher.init({ + appKey: CONFIG.PUSHER.APP_KEY, + cluster: CONFIG.PUSHER.CLUSTER, + authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, + }); +}); + +function scrollToOffset(offset: number) { + const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); + fireEvent.scroll(screen.getByLabelText(hintText), { + nativeEvent: { + contentOffset: { + y: offset, + }, + contentSize: { + // Dimensions of the scrollable content + height: 500, + width: 100, + }, + layoutMeasurement: { + // Dimensions of the device + height: 700, + width: 300, + }, + }, + }); +} + +function navigateToSidebar(): Promise { + const hintText = Localize.translateLocal('accessibilityHints.navigateToChatsList'); + const reportHeaderBackButton = screen.queryByAccessibilityHint(hintText); + if (reportHeaderBackButton) { + fireEvent(reportHeaderBackButton, 'press'); + } + return waitForBatchedUpdates(); +} + +async function navigateToSidebarOption(index: number): Promise { + const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(hintText); + fireEvent(optionRows[index], 'press'); + await waitForBatchedUpdatesWithAct(); +} + +const REPORT_ID = '1'; +const USER_A_ACCOUNT_ID = 1; +const USER_A_EMAIL = 'user_a@test.com'; +const USER_B_ACCOUNT_ID = 2; +const USER_B_EMAIL = 'user_b@test.com'; + +/** + * Sets up a test with a logged in user. Returns the test instance. + */ +function signInAndGetApp(): Promise { + // Render the App and sign in as a test user. + render(); + return waitForBatchedUpdatesWithAct() + .then(async () => { + await waitForBatchedUpdatesWithAct(); + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + await act(async () => { + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }); + return waitForBatchedUpdatesWithAct(); + }) + .then(() => { + User.subscribeToUserEvents(); + return waitForBatchedUpdates(); + }) + .then(async () => { + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastMessageText: 'Test', + participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }); + + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + + return waitForBatchedUpdatesWithAct(); + }); +} + +describe('Pagination', () => { + afterEach(async () => { + await Onyx.clear(); + + // Unsubscribe to pusher channels + PusherHelper.teardown(); + + await waitForBatchedUpdatesWithAct(); + + jest.clearAllMocks(); + }); + + it('opens a chat and load initial messages', async () => { + await signInAndGetApp(); + await navigateToSidebarOption(0); + await act(() => transitionEndCB?.()); + }); +}); diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index ba7398e768a6..3b817fd2da6e 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1,10 +1,9 @@ import Onyx from 'react-native-onyx'; import type {KeyValueMapping} from 'react-native-onyx'; -import type {ReportMetadataPage} from '@src/types/onyx/ReportMetadata'; import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; -import type {Report, ReportAction} from '../../src/types/onyx'; +import type {Report, ReportAction, ReportActionsPages} from '../../src/types/onyx'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; @@ -486,10 +485,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - {firstReportActionID: '17', lastReportActionID: '14'}, - {firstReportActionID: '12', lastReportActionID: '9'}, - {firstReportActionID: '7', lastReportActionID: '1'}, + const pages: ReportActionsPages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], ]; const expectedResult = [ @@ -528,10 +528,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - {firstReportActionID: '17', lastReportActionID: '14'}, - {firstReportActionID: '12', lastReportActionID: '9'}, - {firstReportActionID: '7', lastReportActionID: '1'}, + const pages: ReportActionsPages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], ]; const expectedResult = [ @@ -567,10 +568,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - {firstReportActionID: '17', lastReportActionID: '14'}, - {firstReportActionID: '12', lastReportActionID: '9'}, - {firstReportActionID: '7', lastReportActionID: '1'}, + const pages: ReportActionsPages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], ]; const expectedResult = [ @@ -606,10 +608,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - {firstReportActionID: '17', lastReportActionID: '14'}, - {firstReportActionID: '12', lastReportActionID: '9'}, - {firstReportActionID: '7', lastReportActionID: '1'}, + const pages: ReportActionsPages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], ]; // Expect these sortedReportActions @@ -640,10 +643,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - {firstReportActionID: '17', lastReportActionID: '14'}, - {firstReportActionID: '12', lastReportActionID: '9'}, - {firstReportActionID: '7', lastReportActionID: '1'}, + const pages: ReportActionsPages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], ]; const expectedResult: ReportAction[] = [ @@ -666,7 +670,7 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = []; + const pages: ReportActionsPages = []; // Expect these sortedReportActions const expectedResult = [...input]; @@ -686,7 +690,7 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = []; + const pages: ReportActionsPages = []; // Expect these sortedReportActions const expectedResult: ReportAction[] = []; @@ -716,10 +720,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - {firstReportActionID: '17', lastReportActionID: '14'}, - {firstReportActionID: '12', lastReportActionID: '9'}, - {firstReportActionID: '7', lastReportActionID: '2'}, + const pages: ReportMetadataPages = [ + // Given these pages + ['17', '14'], + ['12', '9'], + ['7', '2'], ]; const expectedResult = [ diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 9cac63e73ad1..d1476175e886 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -13,9 +13,14 @@ type MockFetch = ReturnType & { fail?: () => void; succeed?: () => void; resume?: () => Promise; + mockAPICommand?: (command: string, response: OnyxResponse['onyxData']) => void; }; -type QueueItem = (value: Partial | PromiseLike>) => void; +type QueueItem = { + resolve: (value: Partial | PromiseLike>) => void; + input: RequestInfo; + init?: RequestInit; +}; type FormData = { entries: () => Array<[string, string | Blob]>; @@ -157,11 +162,12 @@ function signOutTestUser() { * - success() - go back to returning a success response */ function getGlobalFetchMock() { - const queue: QueueItem[] = []; + let queue: QueueItem[] = []; + let responses = new Map(); let isPaused = false; let shouldFail = false; - const getResponse = (): Partial => + const getResponse = (input: RequestInfo): Partial => shouldFail ? { ok: true, @@ -169,27 +175,44 @@ function getGlobalFetchMock() { } : { ok: true, - json: () => Promise.resolve({jsonCode: 200}), + json: () => { + const commandMatch = typeof input === 'string' ? input.match(/https:\/\/www.expensify.com.dev\/api\/(\w+)\?/) : null; + const command = commandMatch ? commandMatch[1] : null; + + return Promise.resolve({jsonCode: 200}); + }, }; - const mockFetch: MockFetch = jest.fn().mockImplementation(() => { + const mockFetch: MockFetch = jest.fn().mockImplementation((input: RequestInfo) => { if (!isPaused) { - return Promise.resolve(getResponse()); + return Promise.resolve(getResponse(input)); } return new Promise((resolve) => { - queue.push(resolve); + queue.push({resolve, input}); }); }); + const baseMockReset = mockFetch.mockReset.bind(mockFetch); + mockFetch.mockReset = () => { + baseMockReset(); + queue = []; + responses = new Map(); + isPaused = false; + shouldFail = false; + return mockFetch; + }; + mockFetch.pause = () => (isPaused = true); mockFetch.resume = () => { isPaused = false; - queue.forEach((resolve) => resolve(getResponse())); + queue.forEach(({resolve, input, init}) => resolve(getResponse(input, init))); return waitForBatchedUpdates(); }; mockFetch.fail = () => (shouldFail = true); mockFetch.succeed = () => (shouldFail = false); - + mockFetch.mockAPICommand = (command: string, response: OnyxResponse['onyxData']) => { + responses.set(command, response); + }; return mockFetch as typeof fetch; } From 068f53e76486096af9572f435bae6519abe56b11 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 24 May 2024 14:14:25 -0700 Subject: [PATCH 008/512] Consolidate appversion in enhanceparameters --- src/libs/API/index.ts | 3 --- src/libs/Network/enhanceParameters.ts | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 26b6813100a6..cf93be832314 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -9,7 +9,6 @@ import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; import type OnyxRequest from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -import pkg from '../../../package.json'; import type {ApiRequest, ApiRequestCommandParameters, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). @@ -67,7 +66,6 @@ function write(command: TCommand, apiCommandParam // Assemble the data we'll send to the API const data = { ...apiCommandParameters, - appversion: pkg.version, apiRequestType: CONST.API_REQUEST_TYPE.WRITE, // We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event @@ -130,7 +128,6 @@ function makeRequestWithSideEffects Date: Fri, 24 May 2024 17:48:39 -0400 Subject: [PATCH 009/512] wip --- src/CONST.ts | 1 + src/libs/ReportActionsUtils.ts | 44 ++++++--- src/pages/home/ReportScreen.tsx | 12 ++- tests/unit/ReportActionsUtilsTest.ts | 140 ++++++++------------------- 4 files changed, 79 insertions(+), 118 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e404813b06db..b1cea3d7b1ed 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4779,6 +4779,7 @@ const CONST = { }, PAGINATION_START_ID: '-1', + PAGINATION_END_ID: '-2', } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b3dee3d9f589..7914a9d0064a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -7,6 +7,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActionsPages} from '@src/types/onyx'; import type { ActionName, ChangeLog, @@ -22,7 +23,6 @@ import type { import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {Message, ReportActionBase, ReportActionMessageJSON, ReportActions} from '@src/types/onyx/ReportAction'; -import type {ReportMetadataPage} from '@src/types/onyx/ReportMetadata'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DateUtils from './DateUtils'; @@ -304,23 +304,34 @@ function getCombinedReportActions(reportActions: ReportAction[], transactionThre return getSortedReportActions(filteredReportActions, true); } -type ReportMetadataPageWithIndexes = ReportMetadataPage & { +type PageWithIndexes = { + ids: string[]; + firstReportActionID: string; firstReportActionIndex: number; + lastReportActionID: string; lastReportActionIndex: number; }; -function getPagesWithIndexes(sortedReportActions: ReportAction[], pages: ReportMetadataPage[]): ReportMetadataPageWithIndexes[] { - return pages.map((page) => ({ - ...page, - // TODO: It should be possible to make this O(n) by starting the search at the previous found index. - // TODO: What if reportActionID is not in the list? Could happen if an action is deleted from another device. - firstReportActionIndex: page.firstReportActionID == null ? 0 : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === page.firstReportActionID), - lastReportActionIndex: - page.lastReportActionID == null ? sortedReportActions.length - 1 : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === page.lastReportActionID), - })); +function getPagesWithIndexes(sortedReportActions: ReportAction[], pages: ReportActionsPages): PageWithIndexes[] { + return pages.map((page) => { + const firstReportActionID = page[0]; + const lastReportActionID = page[page.length - 1]; + return { + ids: page, + firstReportActionID, + // TODO: What if reportActionID is not in the list? Could happen if an action is deleted from another device. + firstReportActionIndex: + firstReportActionID === CONST.PAGINATION_START_ID ? 0 : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === firstReportActionID), + lastReportActionID, + lastReportActionIndex: + lastReportActionID === CONST.PAGINATION_END_ID + ? sortedReportActions.length - 1 + : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === lastReportActionID), + }; + }); } -function mergeContinuousPages(sortedReportActions: ReportAction[], pages: ReportMetadataPage[]): ReportMetadataPage[] { +function mergeContinuousPages(sortedReportActions: ReportAction[], pages: ReportActionsPages): ReportActionsPages { const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); // Pages need to be sorted by firstReportActionIndex ascending then by lastReportActionIndex descending. @@ -351,6 +362,8 @@ function mergeContinuousPages(sortedReportActions: ReportAction[], pages: Report firstReportActionIndex: prevPage.firstReportActionIndex, lastReportActionID: page.lastReportActionID, lastReportActionIndex: page.lastReportActionIndex, + // Only add items from prevPage that are not included in page in case of overlap. + ids: prevPage.ids.slice(0, prevPage.ids.indexOf(page.firstReportActionID)).concat(page.ids), }; // eslint-disable-next-line no-continue continue; @@ -360,8 +373,7 @@ function mergeContinuousPages(sortedReportActions: ReportAction[], pages: Report result.push(page); } - // Remove extraneous props. - return result.map((page) => ({firstReportActionID: page.firstReportActionID, lastReportActionID: page.lastReportActionID})); + return result.map((page) => page.ids); } /** @@ -369,14 +381,14 @@ function mergeContinuousPages(sortedReportActions: ReportAction[], pages: Report * See unit tests for example of inputs and expected outputs. * Note: sortedReportActions sorted in descending order */ -function getContinuousReportActionChain(sortedReportActions: ReportAction[], pages: ReportMetadataPage[], id?: string): ReportAction[] { +function getContinuousReportActionChain(sortedReportActions: ReportAction[], pages: ReportActionsPages, id?: string): ReportAction[] { if (pages.length === 0) { return id ? [] : sortedReportActions; } const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); - let page: ReportMetadataPageWithIndexes; + let page: PageWithIndexes; if (id) { const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 17c2d3d76507..1c55fa08acba 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -85,6 +85,9 @@ type ReportScreenOnyxPropsWithoutParentReportAction = { /** The report metadata loading states */ reportMetadata: OnyxEntry; + + /** Pagination data */ + pages: OnyxEntry; }; type OnyxHOCProps = { @@ -144,6 +147,7 @@ function ReportScreen({ isLoadingNewerReportActions: false, hasLoadingNewerReportActionsError: false, }, + pages = [], parentReportActions, accountManagerReportID, markReadyForHydration, @@ -270,8 +274,8 @@ function ReportScreen({ if (!sortedAllReportActions.length) { return []; } - return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportMetadata?.pages ?? [], reportActionIDFromRoute); - }, [reportActionIDFromRoute, sortedAllReportActions, reportMetadata?.pages]); + return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, pages ?? [], reportActionIDFromRoute); + }, [reportActionIDFromRoute, sortedAllReportActions, pages]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. // If we have cached reportActions, they will be shown immediately. @@ -784,6 +788,10 @@ export default withCurrentReportID( hasLoadingNewerReportActionsError: false, }, }, + pages: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${getReportID(route)}`, + initialValue: [], + }, isComposerFullSize: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, initialValue: false, diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 3b817fd2da6e..2d1079401bc5 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -720,11 +720,11 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; - const pages: ReportMetadataPages = [ + const pages: ReportActionsPages = [ // Given these pages - ['17', '14'], - ['12', '9'], - ['7', '2'], + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2'], ]; const expectedResult = [ @@ -747,7 +747,10 @@ describe('ReportActionsUtils', () => { createReportAction('14'), ]; - const pages: ReportMetadataPage[] = [{firstReportActionID: null, lastReportActionID: '14'}]; + const pages: ReportActionsPages = [ + // Given these pages + [CONST.PAGINATION_START_ID, '15', '14'], + ]; const expectedResult = [ // Expect these sortedReportActions @@ -769,7 +772,10 @@ describe('ReportActionsUtils', () => { createReportAction('14'), ]; - const pages: ReportMetadataPage[] = [{firstReportActionID: '17', lastReportActionID: null}]; + const pages: ReportActionsPages = [ + // Given these pages + ['17', '16', CONST.PAGINATION_END_ID], + ]; const expectedResult = [ // Expect these sortedReportActions @@ -786,66 +792,33 @@ describe('ReportActionsUtils', () => { describe('mergeContinuousPages', () => { it('merges continuous pages', () => { const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; - const pages: ReportMetadataPage[] = [ - { - firstReportActionID: '5', - lastReportActionID: '3', - }, - { - firstReportActionID: '3', - lastReportActionID: '1', - }, - ]; - const expectedResult = [ - { - firstReportActionID: '5', - lastReportActionID: '1', - }, + const pages: ReportActionsPages = [ + ['5', '4', '3'], + ['3', '2', '1'], ]; + const expectedResult: ReportActionsPages = [['5', '4', '3', '2', '1']]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); it('merges overlapping pages', () => { const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; - const pages: ReportMetadataPage[] = [ - { - firstReportActionID: '4', - lastReportActionID: '2', - }, - { - firstReportActionID: '3', - lastReportActionID: '1', - }, - ]; - const expectedResult = [ - { - firstReportActionID: '4', - lastReportActionID: '1', - }, + const pages: ReportActionsPages = [ + ['4', '3', '2'], + ['3', '2', '1'], ]; + const expectedResult: ReportActionsPages = [['4', '3', '2', '1']]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); it('merges included pages', () => { const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; - const pages: ReportMetadataPage[] = [ - { - firstReportActionID: '5', - lastReportActionID: '1', - }, - { - firstReportActionID: '5', - lastReportActionID: '2', - }, - ]; - const expectedResult = [ - { - firstReportActionID: '5', - lastReportActionID: '1', - }, + const pages: ReportActionsPages = [ + ['5', '4', '3', '2', '1'], + ['5', '4', '3', '2'], ]; + const expectedResult: ReportActionsPages = [['5', '4', '3', '2', '1']]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); @@ -858,25 +831,13 @@ describe('ReportActionsUtils', () => { createReportAction('2'), createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - { - firstReportActionID: '5', - lastReportActionID: '4', - }, - { - firstReportActionID: '2', - lastReportActionID: '1', - }, + const pages: ReportActionsPages = [ + ['5', '4'], + ['2', '1'], ]; - const expectedResult = [ - { - firstReportActionID: '5', - lastReportActionID: '4', - }, - { - firstReportActionID: '2', - lastReportActionID: '1', - }, + const expectedResult: ReportActionsPages = [ + ['5', '4'], + ['2', '1'], ]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); @@ -894,37 +855,16 @@ describe('ReportActionsUtils', () => { createReportAction('2'), createReportAction('1'), ]; - const pages: ReportMetadataPage[] = [ - { - firstReportActionID: '3', - lastReportActionID: '1', - }, - { - firstReportActionID: '3', - lastReportActionID: '2', - }, - { - firstReportActionID: '6', - lastReportActionID: '5', - }, - { - firstReportActionID: '17', - lastReportActionID: '8', - }, - ]; - const expectedResult = [ - { - firstReportActionID: '17', - lastReportActionID: '8', - }, - { - firstReportActionID: '6', - lastReportActionID: '5', - }, - { - firstReportActionID: '3', - lastReportActionID: '1', - }, + const pages: ReportActionsPages = [ + ['3', '2', '1'], + ['3', '2'], + ['6', '5'], + ['7', '8'], + ]; + const expectedResult: ReportActionsPages = [ + ['7', '8'], + ['6', '5'], + ['3', '2', '1'], ]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); From e4be92c2f3341e2c4a419d0fe524985881705b4d Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 24 May 2024 14:23:23 -0700 Subject: [PATCH 010/512] DRY up applyOptimisticOnyxData --- src/libs/API/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index cf93be832314..c131b7443920 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -40,6 +40,14 @@ type OnyxData = { finallyData?: OnyxUpdate[]; }; +function applyOptimisticOnyxData(onyxData: OnyxData): Omit { + const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; + if (optimisticData) { + Onyx.update(optimisticData); + } + return onyxDataWithoutOptimisticData; +} + /** * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values). * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later. @@ -56,12 +64,8 @@ type OnyxData = { */ function write(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) { Log.info('Called API write', false, {command, ...apiCommandParameters}); - const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; - // Optimistically update Onyx - if (optimisticData) { - Onyx.update(optimisticData); - } + const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); // Assemble the data we'll send to the API const data = { @@ -118,12 +122,8 @@ function makeRequestWithSideEffects { Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); - const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; - // Optimistically update Onyx - if (optimisticData) { - Onyx.update(optimisticData); - } + const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); // Assemble the data we'll send to the API const data = { From d59db4ba4e310c3b3a01236038d360377e63111e Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 24 May 2024 15:02:16 -0700 Subject: [PATCH 011/512] Add API.paginate --- src/libs/API/index.ts | 56 +++++++++++++++++++++++++------ src/libs/Middleware/Pagination.ts | 4 +++ src/types/onyx/Request.ts | 1 + 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index c131b7443920..1f11db7e0c0d 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -62,7 +62,7 @@ function applyOptimisticOnyxData(onyxData: OnyxData): Omit(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) { +function write(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { Log.info('Called API write', false, {command, ...apiCommandParameters}); const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); @@ -121,7 +121,9 @@ function makeRequestWithSideEffects { - Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); + if (apiRequestType === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) { + Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); + } const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); @@ -142,6 +144,21 @@ function makeRequestWithSideEffects(command: TCommand) { + // Ensure all write requests on the sequential queue have finished responding before running read requests. + // Responses from read requests can overwrite the optimistic data inserted by + // write requests that use the same Onyx keys and haven't responded yet. + if (PersistedRequests.getLength() > 0) { + Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`); + } + return SequentialQueue.waitForIdle(); +} + /** * Requests made with this method are not be persisted to disk. If there is no network connectivity, the request is ignored and discarded. * @@ -155,14 +172,33 @@ function makeRequestWithSideEffects(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) { - // Ensure all write requests on the sequential queue have finished responding before running read requests. - // Responses from read requests can overwrite the optimistic data inserted by - // write requests that use the same Onyx keys and haven't responded yet. - if (PersistedRequests.getLength() > 0) { - Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`); - } - SequentialQueue.waitForIdle().then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ)); +function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { + Log.info('[API] Called API.read', false, {command, ...apiCommandParameters}); + validateReadyToRead(command).then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ)); +} + +function paginate(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { + Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters}); + validateReadyToRead(command).then(() => { + const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); + + // Assemble the data we'll send to the API + const data = { + ...apiCommandParameters, + apiRequestType: CONST.API_REQUEST_TYPE.READ, + }; + + // Assemble all the request data we'll be storing + const request: OnyxRequest = { + command, + data, + ...onyxDataWithoutOptimisticData, + isPaginated: true, + }; + + // Return a promise containing the response from HTTPS + return Request.processWithMiddleware(request); + }); } export {write, makeRequestWithSideEffects, read}; diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 99033a043a40..0bd4f17263a5 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -12,6 +12,10 @@ function getReportActions(response: Response, reportID: string) { } const Pagination: Middleware = (requestResponse, request) => { + if (!request.isPaginated) { + return requestResponse; + } + if (request.command === WRITE_COMMANDS.OPEN_REPORT || request.command === READ_COMMANDS.GET_OLDER_ACTIONS || request.command === READ_COMMANDS.GET_NEWER_ACTIONS) { return requestResponse.then((response) => { if (!response?.onyxData) { diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 233519f010fc..b3c3e69fc5ef 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -22,6 +22,7 @@ type RequestData = { resolve?: (value: Response) => void; reject?: (value?: unknown) => void; shouldSkipWebProxy?: boolean; + isPaginated?: boolean; }; type Request = RequestData & OnyxData; From bf0d28225e8f811495c37a2ddffcf186d8ad714a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sat, 25 May 2024 00:04:51 -0400 Subject: [PATCH 012/512] Handle delete / reordered actions, add more tests --- src/libs/ReportActionsUtils.ts | 81 +++++++++++--- tests/unit/ReportActionsUtilsTest.ts | 156 +++++++++++++++++++++++++-- 2 files changed, 213 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7914a9d0064a..178279942f12 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -312,28 +312,77 @@ type PageWithIndexes = { lastReportActionIndex: number; }; +/** + * Finds the id, index in sortedReportActions and index in the page of the first valid report action in the given page. + */ +function findFirstReportAction(sortedReportActions: ReportAction[], page: string[]): {id: string; index: number} | null { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < page.length; i++) { + const id = page[i]; + if (id === CONST.PAGINATION_START_ID) { + return {id, index: 0}; + } + const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); + if (index !== -1) { + return {id, index}; + } + } + return null; +} + +/** + * Finds the id, index in sortedReportActions and index in the page of the last valid report action in the given page. + */ +function findLastReportAction(sortedReportActions: ReportAction[], page: string[]): {id: string; index: number} | null { + for (let i = page.length - 1; i >= 0; i--) { + const id = page[i]; + if (id === CONST.PAGINATION_END_ID) { + return {id, index: sortedReportActions.length - 1}; + } + const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); + if (index !== -1) { + return {id, index}; + } + } + return null; +} + function getPagesWithIndexes(sortedReportActions: ReportAction[], pages: ReportActionsPages): PageWithIndexes[] { - return pages.map((page) => { - const firstReportActionID = page[0]; - const lastReportActionID = page[page.length - 1]; - return { - ids: page, - firstReportActionID, - // TODO: What if reportActionID is not in the list? Could happen if an action is deleted from another device. - firstReportActionIndex: - firstReportActionID === CONST.PAGINATION_START_ID ? 0 : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === firstReportActionID), - lastReportActionID, - lastReportActionIndex: - lastReportActionID === CONST.PAGINATION_END_ID - ? sortedReportActions.length - 1 - : sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === lastReportActionID), - }; - }); + return pages + .map((page) => { + let firstReportAction = findFirstReportAction(sortedReportActions, page); + let lastReportAction = findLastReportAction(sortedReportActions, page); + + // If all actions in the page are not found it will be removed. + if (firstReportAction === null || lastReportAction === null) { + return null; + } + + // In case actions were reordered, we need to swap them. + if (firstReportAction.index > lastReportAction.index) { + const temp = firstReportAction; + firstReportAction = lastReportAction; + lastReportAction = temp; + } + + return { + ids: sortedReportActions.slice(firstReportAction.index, lastReportAction.index + 1).map((reportAction) => reportAction.reportActionID), + firstReportActionID: firstReportAction.id, + firstReportActionIndex: firstReportAction.index, + lastReportActionID: lastReportAction.id, + lastReportActionIndex: lastReportAction.index, + }; + }) + .filter((page) => page !== null) as PageWithIndexes[]; } function mergeContinuousPages(sortedReportActions: ReportAction[], pages: ReportActionsPages): ReportActionsPages { const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); + if (pagesWithIndexes.length === 0) { + return []; + } + // Pages need to be sorted by firstReportActionIndex ascending then by lastReportActionIndex descending. const sortedPages = pagesWithIndexes.sort((a, b) => { if (a.firstReportActionIndex !== b.firstReportActionIndex) { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 2d1079401bc5..63f7baed3b84 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -791,40 +791,74 @@ describe('ReportActionsUtils', () => { describe('mergeContinuousPages', () => { it('merges continuous pages', () => { - const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; const pages: ReportActionsPages = [ + // Given these pages ['5', '4', '3'], ['3', '2', '1'], ]; - const expectedResult: ReportActionsPages = [['5', '4', '3', '2', '1']]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['5', '4', '3', '2', '1'], + ]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); it('merges overlapping pages', () => { - const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; const pages: ReportActionsPages = [ + // Given these pages ['4', '3', '2'], ['3', '2', '1'], ]; - const expectedResult: ReportActionsPages = [['4', '3', '2', '1']]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['4', '3', '2', '1'], + ]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); it('merges included pages', () => { - const sortedReportActions = [createReportAction('5'), createReportAction('4'), createReportAction('3'), createReportAction('2'), createReportAction('1')]; + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('5'), + createReportAction('4'), + createReportAction('3'), + createReportAction('2'), + createReportAction('1'), + ]; const pages: ReportActionsPages = [ + // Given these pages ['5', '4', '3', '2', '1'], ['5', '4', '3', '2'], ]; - const expectedResult: ReportActionsPages = [['5', '4', '3', '2', '1']]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['5', '4', '3', '2', '1'], + ]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); it('do not merge separate pages', () => { const sortedReportActions = [ + // Given these sortedReportActions createReportAction('5'), createReportAction('4'), // Gap @@ -832,10 +866,12 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; const pages: ReportActionsPages = [ + // Given these pages ['5', '4'], ['2', '1'], ]; const expectedResult: ReportActionsPages = [ + // Expect these pages ['5', '4'], ['2', '1'], ]; @@ -845,6 +881,7 @@ describe('ReportActionsUtils', () => { it('sorts pages', () => { const sortedReportActions = [ + // Given these sortedReportActions createReportAction('9'), createReportAction('8'), // Gap @@ -856,16 +893,119 @@ describe('ReportActionsUtils', () => { createReportAction('1'), ]; const pages: ReportActionsPages = [ + // Given these pages ['3', '2', '1'], ['3', '2'], ['6', '5'], - ['7', '8'], + ['9', '8'], ]; const expectedResult: ReportActionsPages = [ - ['7', '8'], + // Expect these pages + ['9', '8'], + ['6', '5'], + ['3', '2', '1'], + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles actions that no longer exist', () => { + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('4'), + createReportAction('3'), + ]; + const pages: ReportActionsPages = [ + // Given these pages + ['6', '5', '4', '3', '2', '1'], + ]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['4', '3'], + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('removes pages that are empty', () => { + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('4'), + ]; + const pages: ReportActionsPages = [ + // Given these pages ['6', '5'], ['3', '2', '1'], ]; + + // Expect these pages + const expectedResult: ReportActionsPages = []; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles pages with a single action', () => { + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('4'), + createReportAction('2'), + ]; + const pages: ReportActionsPages = [ + // Given these pages + ['4'], + ['2'], + ['2'], + ]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['4'], + ['2'], + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles out of order ids', () => { + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('2'), + createReportAction('1'), + createReportAction('3'), + createReportAction('4'), + ]; + const pages: ReportActionsPages = [ + // Given these pages + ['2', '1'], + ['1', '3'], + ['4'], + ]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['2', '1', '3'], + ['4'], + ]; + const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles basic reordering', () => { + const sortedReportActions = [ + // Given these sortedReportActions + createReportAction('1'), + createReportAction('2'), + createReportAction('4'), + createReportAction('5'), + ]; + const pages: ReportActionsPages = [ + // Given these pages + ['5', '4'], + ['2', '1'], + ]; + const expectedResult: ReportActionsPages = [ + // Expect these pages + ['1', '2'], + ['4', '5'], + ]; const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); expect(result).toStrictEqual(expectedResult); }); From 450189fc05895d693bd1e9e35fb177c4f9cdeec4 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 24 May 2024 16:23:58 -0700 Subject: [PATCH 013/512] Set up bulk of generalized Middleware --- src/libs/API/index.ts | 17 +++++++++-- src/libs/Middleware/Pagination.ts | 50 +++++++++++++++++++++++++++++-- src/libs/Middleware/types.ts | 7 ++++- src/types/onyx/Request.ts | 14 +++++++-- 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 1f11db7e0c0d..f58d88cccade 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -8,6 +8,7 @@ import * as Request from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; import type OnyxRequest from '@src/types/onyx/Request'; +import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import type {ApiRequest, ApiRequestCommandParameters, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; @@ -177,7 +178,15 @@ function read(command: TCommand, apiCommandParamet validateReadyToRead(command).then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ)); } -function paginate(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { +function paginate( + command: TCommand, + apiCommandParameters: ApiRequestCommandParameters[TCommand], + getItemsFromResponse: (response: Response) => Record, + sortItems: (items: Record) => TResource[], + getItemID: (item: TResource) => string, + isInitialRequest = false, + onyxData: OnyxData = {}, +): void { Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters}); validateReadyToRead(command).then(() => { const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); @@ -189,11 +198,15 @@ function paginate(command: TCommand, apiCommandPar }; // Assemble all the request data we'll be storing - const request: OnyxRequest = { + const request: PaginatedRequest = { command, data, ...onyxDataWithoutOptimisticData, isPaginated: true, + getItemsFromResponse, + sortItems, + getItemID, + isInitialRequest, }; // Return a promise containing the response from HTTPS diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 0bd4f17263a5..c26559905100 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -1,21 +1,67 @@ // TODO: Is this a legit use case for exposing `OnyxCache`, or should we use `Onyx.connect`? +import fastMerge from 'expensify-common/lib/fastMerge'; +import Onyx from 'react-native-onyx'; import OnyxCache from 'react-native-onyx/dist/OnyxCache'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import Log from '@libs/Log'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActions, ReportActionsPages, Response} from '@src/types/onyx'; +import type {ReportActions, ReportActionsPages, Request, Response} from '@src/types/onyx'; +import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Middleware from './types'; +function isPaginatedRequest(request: Request | PaginatedRequest): request is PaginatedRequest { + return 'isPaginated' in request && request.isPaginated; +} + function getReportActions(response: Response, reportID: string) { return response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions | undefined; } const Pagination: Middleware = (requestResponse, request) => { - if (!request.isPaginated) { + if (!isPaginatedRequest(request)) { return requestResponse; } + const {pageKey, getItemsFromResponse, sortItems, getItemID, isInitialRequest} = request; + return requestResponse.then((response) => { + if (!response?.onyxData) { + return Promise.resolve(response); + } + + // Create a new page based on the response + const items = getItemsFromResponse(response); + const sortedItems = sortItems(items); + if (sortedItems.length === 0) { + // Must have at least 1 action to create a page. + Log.hmmm(`[Pagination] Did not receive any items in the response to ${request.command}`); + return Promise.resolve(response); + } + + const sortedItemIDs = sortedItems.map((item) => getItemID(item)); + if (isInitialRequest) { + sortedItemIDs.unshift(CONST.PAGINATION_START_ID); + } + + // TODO: Provide key for the resource in the paginatedRequest. Also, we can probably derive the "pages" key from that resource. + const existingItems = OnyxCache.getValue(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + const allItems = fastMerge(existingItems, items, true); + const sortedAllItems = sortItems(allItems); + + const existingPages = OnyxCache.getValue(pageKey); + // TODO: generalize this function + const mergedPages = ReportActionsUtils.mergeContinuousPages(); + + response.onyxData.push({ + key: pageKey, + onyxMethod: Onyx.METHOD.SET, + value: mergedPages, + }); + + return Promise.resolve(response); + }); + if (request.command === WRITE_COMMANDS.OPEN_REPORT || request.command === READ_COMMANDS.GET_OLDER_ACTIONS || request.command === READ_COMMANDS.GET_NEWER_ACTIONS) { return requestResponse.then((response) => { if (!response?.onyxData) { diff --git a/src/libs/Middleware/types.ts b/src/libs/Middleware/types.ts index 4cc0a1cc1026..148e9f361234 100644 --- a/src/libs/Middleware/types.ts +++ b/src/libs/Middleware/types.ts @@ -1,6 +1,11 @@ import type Request from '@src/types/onyx/Request'; +import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -type Middleware = (response: Promise, request: Request, isFromSequentialQueue: boolean) => Promise; +type Middleware = ( + response: Promise, + request: Request | PaginatedRequest, + isFromSequentialQueue: boolean, +) => Promise; export default Middleware; diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index b3c3e69fc5ef..2b4f49e49e7d 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -1,4 +1,5 @@ import type {OnyxUpdate} from 'react-native-onyx'; +import type {OnyxKey} from '@src/ONYXKEYS'; import type Response from './Response'; type OnyxData = { @@ -22,10 +23,19 @@ type RequestData = { resolve?: (value: Response) => void; reject?: (value?: unknown) => void; shouldSkipWebProxy?: boolean; - isPaginated?: boolean; +}; + +type PaginatedRequestData = { + isPaginated: true; + pageKey: TPageKey; + getItemsFromResponse: (response: Response) => Record; + sortItems: (items: Record) => TResource[]; + getItemID: (item: TResource) => string; + isInitialRequest: boolean; }; type Request = RequestData & OnyxData; +type PaginatedRequest = Request & PaginatedRequestData; export default Request; -export type {OnyxData, RequestType}; +export type {OnyxData, RequestType, PaginatedRequest}; From 867abfc8c3469609786746698dbf24fc8bd56287 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 24 May 2024 18:15:26 -0700 Subject: [PATCH 014/512] Implement PaginationUtils.mergeContinuousPages --- src/ONYXKEYS.ts | 7 +-- src/libs/API/index.ts | 15 ++++-- src/libs/Middleware/Pagination.ts | 76 +++++++------------------------ src/libs/PaginationUtils.ts | 71 +++++++++++++++++++++++++++++ src/types/onyx/Pages.ts | 3 ++ src/types/onyx/Request.ts | 15 +++--- src/types/onyx/index.ts | 2 + 7 files changed, 113 insertions(+), 76 deletions(-) create mode 100644 src/libs/PaginationUtils.ts create mode 100644 src/types/onyx/Pages.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f8bfb1edf698..eac65ff6e7d8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,4 @@ -import type {ValueOf} from 'type-fest'; +import type {ConditionalKeys, ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; @@ -558,7 +558,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; - [ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.ReportActionsPages; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; @@ -692,6 +692,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping; type OnyxValueKey = keyof OnyxValuesMapping; type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey; +type OnyxPagesKey = ConditionalKeys; type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ @@ -699,4 +700,4 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: type AssertOnyxKeys = AssertTypesEqual; export default ONYXKEYS; -export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxValueKey, OnyxValues}; +export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues}; diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index f58d88cccade..18e631280d2a 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -7,6 +7,7 @@ import * as Pusher from '@libs/Pusher/pusher'; import * as Request from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; +import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; @@ -178,12 +179,14 @@ function read(command: TCommand, apiCommandParamet validateReadyToRead(command).then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ)); } -function paginate( +function paginate( command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], - getItemsFromResponse: (response: Response) => Record, - sortItems: (items: Record) => TResource[], - getItemID: (item: TResource) => string, + resourceKey: TResourceKey, + pageKey: TPageKey, + getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey], + sortItems: (items: OnyxValues[TResourceKey]) => Array, + getItemID: (item: OnyxValues[TResourceKey]) => string, isInitialRequest = false, onyxData: OnyxData = {}, ): void { @@ -198,11 +201,13 @@ function paginate( }; // Assemble all the request data we'll be storing - const request: PaginatedRequest = { + const request: PaginatedRequest = { command, data, ...onyxDataWithoutOptimisticData, isPaginated: true, + resourceKey, + pageKey, getItemsFromResponse, sortItems, getItemID, diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index c26559905100..f234bdd589f2 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -2,16 +2,18 @@ import fastMerge from 'expensify-common/lib/fastMerge'; import Onyx from 'react-native-onyx'; import OnyxCache from 'react-native-onyx/dist/OnyxCache'; -import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import PaginationUtils from '@libs/PaginationUtils'; import CONST from '@src/CONST'; +import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActions, ReportActionsPages, Request, Response} from '@src/types/onyx'; +import type {Pages, ReportActions, Request, Response} from '@src/types/onyx'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Middleware from './types'; -function isPaginatedRequest(request: Request | PaginatedRequest): request is PaginatedRequest { +function isPaginatedRequest( + request: Request | PaginatedRequest, +): request is PaginatedRequest { return 'isPaginated' in request && request.isPaginated; } @@ -24,34 +26,34 @@ const Pagination: Middleware = (requestResponse, request) => { return requestResponse; } - const {pageKey, getItemsFromResponse, sortItems, getItemID, isInitialRequest} = request; + const {resourceKey, pageKey, getItemsFromResponse, sortItems, getItemID, isInitialRequest} = request; return requestResponse.then((response) => { if (!response?.onyxData) { return Promise.resolve(response); } // Create a new page based on the response - const items = getItemsFromResponse(response); - const sortedItems = sortItems(items); - if (sortedItems.length === 0) { + const pageItems = getItemsFromResponse(response); + const sortedPageItems = sortItems(pageItems); + if (sortedPageItems.length === 0) { // Must have at least 1 action to create a page. Log.hmmm(`[Pagination] Did not receive any items in the response to ${request.command}`); return Promise.resolve(response); } - const sortedItemIDs = sortedItems.map((item) => getItemID(item)); + const newPage = sortedPageItems.map((item) => getItemID(item)); if (isInitialRequest) { - sortedItemIDs.unshift(CONST.PAGINATION_START_ID); + newPage.unshift(CONST.PAGINATION_START_ID); } // TODO: Provide key for the resource in the paginatedRequest. Also, we can probably derive the "pages" key from that resource. - const existingItems = OnyxCache.getValue(ONYXKEYS.COLLECTION.REPORT_ACTIONS); - const allItems = fastMerge(existingItems, items, true); + const existingItems = OnyxCache.getValue(resourceKey) as OnyxValues[typeof resourceKey]; + const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); - const existingPages = OnyxCache.getValue(pageKey); + const existingPages = OnyxCache.getValue(pageKey) as Pages; // TODO: generalize this function - const mergedPages = ReportActionsUtils.mergeContinuousPages(); + const mergedPages = PaginationUtils.mergeContinuousPages(sortedAllItems, [...existingPages, newPage], getItemID); response.onyxData.push({ key: pageKey, @@ -61,52 +63,6 @@ const Pagination: Middleware = (requestResponse, request) => { return Promise.resolve(response); }); - - if (request.command === WRITE_COMMANDS.OPEN_REPORT || request.command === READ_COMMANDS.GET_OLDER_ACTIONS || request.command === READ_COMMANDS.GET_NEWER_ACTIONS) { - return requestResponse.then((response) => { - if (!response?.onyxData) { - return Promise.resolve(response); - } - - const reportID = request.data?.reportID as string | undefined; - if (!reportID) { - // TODO: Should not happen, should we throw? - return Promise.resolve(response); - } - - const reportActionID = request.data?.reportActionID as string | undefined; - - // Create a new page based on the response actions. - const pageReportActions = getReportActions(response, reportID); - const pageSortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(pageReportActions, true); - if (pageSortedReportActions.length === 0) { - // Must have at least 1 action to create a page. - return Promise.resolve(response); - } - const newPage = pageSortedReportActions.map((action) => action.reportActionID); - if (reportActionID == null) { - newPage.unshift(CONST.PAGINATION_START_ID); - } - - const reportActions = OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`) as ReportActions | undefined; - // TODO: Do we need to do proper merge here or this is ok? - const allReportActions = {...reportActions, ...pageReportActions}; - const allSortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - - const pages = (OnyxCache.getValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`) ?? []) as ReportActionsPages; - const newPages = ReportActionsUtils.mergeContinuousPages(allSortedReportActions, [...pages, newPage]); - - response.onyxData.push({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, - onyxMethod: 'merge', - value: newPages, - }); - - return Promise.resolve(response); - }); - } - - return requestResponse; }; export default Pagination; diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts new file mode 100644 index 000000000000..6490c7a86b96 --- /dev/null +++ b/src/libs/PaginationUtils.ts @@ -0,0 +1,71 @@ +import CONST from '@src/CONST'; +import type Pages from '@src/types/onyx/Pages'; + +type PageWithIndex = { + ids: string[]; + firstID: string; + firstIndex: number; + lastID: string; + lastIndex: number; +}; + +function getPagesWithIndexes(sortedItems: TResource[], pages: Pages, getID: (item: TResource) => string): PageWithIndex[] { + return pages.map((page) => { + const firstID = page[0]; + const lastID = page[page.length - 1]; + return { + ids: page, + firstID, + // TODO: What if the ID is not in the list? Could happen if an action is deleted from another device. + firstIndex: firstID === CONST.PAGINATION_START_ID ? 0 : sortedItems.findIndex((item) => getID(item) === firstID), + lastID, + lastIndex: lastID === CONST.PAGINATION_END_ID ? sortedItems.length - 1 : sortedItems.findIndex((item) => getID(item) === lastID), + }; + }); +} + +function mergeContinuousPages(sortedItems: TResource[], pages: Pages, getItemID: (item: TResource) => string): Pages { + const pagesWithIndexes = getPagesWithIndexes(sortedItems, pages, getItemID); + + // Pages need to be sorted by firstIndex ascending then by lastIndex descending + const sortedPages = pagesWithIndexes.sort((a, b) => { + if (a.firstIndex !== b.firstIndex) { + return a.firstIndex - b.firstIndex; + } + return b.lastIndex - a.lastIndex; + }); + + const result = [sortedPages[0]]; + for (let i = 1; i < sortedPages.length; i++) { + const page = sortedPages[i]; + const prevPage = sortedPages[i - 1]; + + // Current page is inside the previous page, skip + if (page.lastIndex <= prevPage.lastIndex) { + // eslint-disable-next-line no-continue + continue; + } + + // Current page overlaps with the previous page, merge. + // This happens if the ids from the current page and previous page are the same or if the indexes overlap + if (page.firstID === prevPage.lastID || page.firstIndex < prevPage.lastIndex) { + result[result.length - 1] = { + firstID: prevPage.firstID, + firstIndex: prevPage.firstIndex, + lastID: page.lastID, + lastIndex: page.lastIndex, + // Only add items from prevPage that are not included in page in case of overlap. + ids: prevPage.ids.slice(0, prevPage.ids.indexOf(page.firstID)).concat(page.ids), + }; + // eslint-disable-next-line no-continue + continue; + } + + // No overlap, add the current page as is. + result.push(page); + } + + return result.map((page) => page.ids); +} + +export default {mergeContinuousPages}; diff --git a/src/types/onyx/Pages.ts b/src/types/onyx/Pages.ts new file mode 100644 index 000000000000..bc95c20c85aa --- /dev/null +++ b/src/types/onyx/Pages.ts @@ -0,0 +1,3 @@ +type Pages = string[][]; + +export default Pages; diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 2b4f49e49e7d..1c7cbdc84c83 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -1,5 +1,5 @@ import type {OnyxUpdate} from 'react-native-onyx'; -import type {OnyxKey} from '@src/ONYXKEYS'; +import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; import type Response from './Response'; type OnyxData = { @@ -25,17 +25,16 @@ type RequestData = { shouldSkipWebProxy?: boolean; }; -type PaginatedRequestData = { +type Request = RequestData & OnyxData; +type PaginatedRequest = Request & { isPaginated: true; + resourceKey: TResourceKey; pageKey: TPageKey; - getItemsFromResponse: (response: Response) => Record; - sortItems: (items: Record) => TResource[]; - getItemID: (item: TResource) => string; + getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey]; + sortItems: (items: OnyxValues[TResourceKey]) => Array; + getItemID: (item: OnyxValues[TResourceKey]) => string; isInitialRequest: boolean; }; -type Request = RequestData & OnyxData; -type PaginatedRequest = Request & PaginatedRequestData; - export default Request; export type {OnyxData, RequestType, PaginatedRequest}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 8accfca8b16e..f10bd27efa0a 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -30,6 +30,7 @@ import type Network from './Network'; import type NewGroupChatDraft from './NewGroupChatDraft'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import type {DecisionName, OriginalMessageIOU} from './OriginalMessage'; +import type Pages from './Pages'; import type PersonalBankAccount from './PersonalBankAccount'; import type {PersonalDetailsList, PersonalDetailsMetadata} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; @@ -112,6 +113,7 @@ export type { Network, OnyxUpdateEvent, OnyxUpdatesFromServer, + Pages, PersonalBankAccount, PersonalDetails, PersonalDetailsList, From 780f63146ed43e9463c12fe08266979d567a62c1 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 27 May 2024 12:36:19 -0700 Subject: [PATCH 015/512] Implement API.paginate for GetOlderActions and GetNewerActions --- src/libs/API/index.ts | 10 +++++----- src/libs/Middleware/Pagination.ts | 13 ++++--------- src/libs/actions/Report.ts | 24 ++++++++++++++++++++++-- src/types/onyx/Request.ts | 6 +++--- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 18e631280d2a..c997833097e2 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -179,14 +179,14 @@ function read(command: TCommand, apiCommandParamet validateReadyToRead(command).then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ)); } -function paginate( +function paginate( command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], resourceKey: TResourceKey, pageKey: TPageKey, getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey], - sortItems: (items: OnyxValues[TResourceKey]) => Array, - getItemID: (item: OnyxValues[TResourceKey]) => string, + sortItems: (items: OnyxValues[TResourceKey]) => TResource[], + getItemID: (item: TResource) => string, isInitialRequest = false, onyxData: OnyxData = {}, ): void { @@ -201,7 +201,7 @@ function paginate = { + const request: PaginatedRequest = { command, data, ...onyxDataWithoutOptimisticData, @@ -219,4 +219,4 @@ function paginate( - request: Request | PaginatedRequest, -): request is PaginatedRequest { +function isPaginatedRequest( + request: Request | PaginatedRequest, +): request is PaginatedRequest { return 'isPaginated' in request && request.isPaginated; } -function getReportActions(response: Response, reportID: string) { - return response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions | undefined; -} - const Pagination: Middleware = (requestResponse, request) => { if (!isPaginatedRequest(request)) { return requestResponse; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 6722575f2661..e7c4d02be085 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1077,7 +1077,17 @@ function getOlderActions(reportID: string, reportActionID: string) { reportActionID, }; - API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData}); + API.paginate( + READ_COMMANDS.GET_OLDER_ACTIONS, + parameters, + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + (reportAction) => reportAction.reportActionID, + false, + {optimisticData, successData, failureData}, + ); } /** @@ -1122,7 +1132,17 @@ function getNewerActions(reportID: string, reportActionID: string) { reportActionID, }; - API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData}); + API.paginate( + READ_COMMANDS.GET_NEWER_ACTIONS, + parameters, + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + (reportAction) => reportAction.reportActionID, + false, + {optimisticData, successData, failureData}, + ); } /** diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 1c7cbdc84c83..07a9780b541d 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -26,13 +26,13 @@ type RequestData = { }; type Request = RequestData & OnyxData; -type PaginatedRequest = Request & { +type PaginatedRequest = Request & { isPaginated: true; resourceKey: TResourceKey; pageKey: TPageKey; getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey]; - sortItems: (items: OnyxValues[TResourceKey]) => Array; - getItemID: (item: OnyxValues[TResourceKey]) => string; + sortItems: (items: OnyxValues[TResourceKey]) => TResource[]; + getItemID: (item: TResource) => string; isInitialRequest: boolean; }; From 23558ae147997b583de2c2279762e00d4c7ed332 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 27 May 2024 15:04:46 -0700 Subject: [PATCH 016/512] Make API.pagination work for non-read request types --- src/libs/API/index.ts | 189 ++++++++++++++------------ src/libs/API/types.ts | 9 +- src/libs/Network/enhanceParameters.ts | 6 + 3 files changed, 114 insertions(+), 90 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index c997833097e2..ff99fa3a3d28 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -1,9 +1,9 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {SetRequired} from 'type-fest'; import Log from '@libs/Log'; import * as Middleware from '@libs/Middleware'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; -import * as Pusher from '@libs/Pusher/pusher'; import * as Request from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; @@ -11,7 +11,7 @@ import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -import type {ApiRequest, ApiRequestCommandParameters, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; +import type {ApiCommand, ApiRequest, ApiRequestCommandParameters, CommandForRequestType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -42,12 +42,58 @@ type OnyxData = { finallyData?: OnyxUpdate[]; }; -function applyOptimisticOnyxData(onyxData: OnyxData): Omit { +/** + * Prepare the request to be sent. Bind data together with request metadata and apply optimistic Onyx data. + */ +function prepareRequest(command: TCommand, type: ApiRequest, params: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): OnyxRequest { + Log.info('[API] Preparing request', false, {command, type}); + const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; if (optimisticData) { + Log.info('[API] Applying optimistic data', false, {command, type}); Onyx.update(optimisticData); } - return onyxDataWithoutOptimisticData; + + // Prepare the data we'll send to the API + const data = { + ...params, + apiRequestType: type, + }; + + // Assemble all request metadata (used by middlewares, and for persisted requests stored in Onyx) + const request: SetRequired = { + command, + data, + ...onyxDataWithoutOptimisticData, + }; + + // This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650 + if (type === CONST.API_REQUEST_TYPE.WRITE) { + request.data.shouldRetry = true; + request.data.canCancel = true; + } + + return request; +} + +/** + * Process a prepared request according to its type. + */ +function processRequest(request: OnyxRequest, type: ApiRequest): Promise { + // Write commands can be saved and retried, so push it to the SequentialQueue + if (type === CONST.API_REQUEST_TYPE.WRITE) { + SequentialQueue.push(request); + return Promise.resolve(); + } + + // Read requests are processed right away, but don't return the response to the caller + if (type === CONST.API_REQUEST_TYPE.READ) { + Request.processWithMiddleware(request); + return Promise.resolve(); + } + + // Requests with side effects process right away, and return the response to the caller + return Request.processWithMiddleware(request); } /** @@ -65,35 +111,9 @@ function applyOptimisticOnyxData(onyxData: OnyxData): Omit(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { - Log.info('Called API write', false, {command, ...apiCommandParameters}); - - const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); - - // Assemble the data we'll send to the API - const data = { - ...apiCommandParameters, - apiRequestType: CONST.API_REQUEST_TYPE.WRITE, - - // We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event - // is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775. - pusherSocketID: Pusher.getPusherSocketID(), - }; - - // Assemble all the request data we'll be storing in the queue - const request: OnyxRequest = { - command, - data: { - ...data, - - // This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650 - shouldRetry: true, - canCancel: true, - }, - ...onyxDataWithoutOptimisticData, - }; - - // Write commands can be saved and retried, so push it to the SequentialQueue - SequentialQueue.push(request); + Log.info('[API] Called API write', false, {command, ...apiCommandParameters}); + const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData); + processRequest(request, CONST.API_REQUEST_TYPE.WRITE); } /** @@ -117,33 +137,16 @@ function write(command: TCommand, apiCommandParam * response back to the caller or to trigger reconnection callbacks when re-authentication is required. * @returns */ -function makeRequestWithSideEffects( +function makeRequestWithSideEffects( command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}, - apiRequestType: ApiRequest = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, ): Promise { - if (apiRequestType === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) { - Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); - } - - const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); - - // Assemble the data we'll send to the API - const data = { - ...apiCommandParameters, - apiRequestType, - }; - - // Assemble all the request data we'll be storing - const request: OnyxRequest = { - command, - data, - ...onyxDataWithoutOptimisticData, - }; + Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); + const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData); // Return a promise containing the response from HTTPS - return Request.processWithMiddleware(request); + return processRequest(request, CONST.API_REQUEST_TYPE.WRITE); } /** @@ -176,47 +179,55 @@ function validateReadyToRead(command: TCommand) { */ function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { Log.info('[API] Called API.read', false, {command, ...apiCommandParameters}); - validateReadyToRead(command).then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ)); + + validateReadyToRead(command).then(() => { + const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData); + processRequest(request, CONST.API_REQUEST_TYPE.READ); + }); } -function paginate( +type PaginationConfig = { + resourceKey: TResourceKey; + pageKey: TPageKey; + getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey]; + sortItems: (items: OnyxValues[TResourceKey]) => TResource[]; + getItemID: (item: TResource) => string; + isInitialRequest: boolean; +}; + +function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( + type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], - resourceKey: TResourceKey, - pageKey: TPageKey, - getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey], - sortItems: (items: OnyxValues[TResourceKey]) => TResource[], - getItemID: (item: TResource) => string, - isInitialRequest = false, - onyxData: OnyxData = {}, -): void { + onyxData: OnyxData, + config: PaginationConfig, +): TRequestType extends typeof CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS ? Promise : void; +function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( + type: TRequestType, + command: TCommand, + apiCommandParameters: ApiRequestCommandParameters[TCommand], + onyxData: OnyxData, + config: PaginationConfig, +): Promise | void { Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters}); - validateReadyToRead(command).then(() => { - const onyxDataWithoutOptimisticData = applyOptimisticOnyxData(onyxData); - - // Assemble the data we'll send to the API - const data = { - ...apiCommandParameters, - apiRequestType: CONST.API_REQUEST_TYPE.READ, - }; - - // Assemble all the request data we'll be storing - const request: PaginatedRequest = { - command, - data, - ...onyxDataWithoutOptimisticData, + const request: PaginatedRequest = { + ...prepareRequest(command, type, apiCommandParameters, onyxData), + ...config, + ...{ isPaginated: true, - resourceKey, - pageKey, - getItemsFromResponse, - sortItems, - getItemID, - isInitialRequest, - }; - - // Return a promise containing the response from HTTPS - return Request.processWithMiddleware(request); - }); + }, + }; + + if (type === CONST.API_REQUEST_TYPE.WRITE) { + processRequest(request, type); + return; + } + + if (type === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) { + return processRequest(request, type); + } + + validateReadyToRead(command as ReadCommand).then(() => processRequest(request, type)); } export {write, makeRequestWithSideEffects, read, paginate}; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 08bc5eddd087..c4a15c47f431 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -554,4 +554,11 @@ type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameter export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS}; -export type {ApiRequest, ApiRequestCommandParameters, WriteCommand, ReadCommand, SideEffectRequestCommand}; +type ApiCommand = WriteCommand | ReadCommand | SideEffectRequestCommand; +type CommandForRequestType = TRequestType extends typeof CONST.API_REQUEST_TYPE.WRITE + ? WriteCommand + : TRequestType extends typeof CONST.API_REQUEST_TYPE.READ + ? ReadCommand + : SideEffectRequestCommand; + +export type {ApiCommand, ApiRequest, ApiRequestCommandParameters, CommandForRequestType, WriteCommand, ReadCommand, SideEffectRequestCommand}; diff --git a/src/libs/Network/enhanceParameters.ts b/src/libs/Network/enhanceParameters.ts index f7fb857fba29..2bebb0470296 100644 --- a/src/libs/Network/enhanceParameters.ts +++ b/src/libs/Network/enhanceParameters.ts @@ -1,5 +1,6 @@ import * as Environment from '@libs/Environment/Environment'; import getPlatform from '@libs/getPlatform'; +import * as Pusher from '@libs/Pusher/pusher'; import CONFIG from '@src/CONFIG'; import {version as pkgVersion} from '../../../package.json'; import * as NetworkStore from './NetworkStore'; @@ -37,6 +38,11 @@ export default function enhanceParameters(command: string, parameters: Record Date: Mon, 27 May 2024 15:47:39 -0700 Subject: [PATCH 017/512] Rename CommandForRequestType --- src/libs/API/index.ts | 10 +++++----- src/libs/API/types.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index ff99fa3a3d28..080ea7ae1db1 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -11,7 +11,7 @@ import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -import type {ApiCommand, ApiRequest, ApiRequestCommandParameters, CommandForRequestType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; +import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -45,7 +45,7 @@ type OnyxData = { /** * Prepare the request to be sent. Bind data together with request metadata and apply optimistic Onyx data. */ -function prepareRequest(command: TCommand, type: ApiRequest, params: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): OnyxRequest { +function prepareRequest(command: TCommand, type: ApiRequestType, params: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): OnyxRequest { Log.info('[API] Preparing request', false, {command, type}); const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; @@ -79,7 +79,7 @@ function prepareRequest(command: TCommand, type: Ap /** * Process a prepared request according to its type. */ -function processRequest(request: OnyxRequest, type: ApiRequest): Promise { +function processRequest(request: OnyxRequest, type: ApiRequestType): Promise { // Write commands can be saved and retried, so push it to the SequentialQueue if (type === CONST.API_REQUEST_TYPE.WRITE) { SequentialQueue.push(request); @@ -195,14 +195,14 @@ type PaginationConfig, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( +function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData, config: PaginationConfig, ): TRequestType extends typeof CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS ? Promise : void; -function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( +function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c4a15c47f431..c2e9d21c33f7 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -5,7 +5,7 @@ import type * as Parameters from './parameters'; import type SignInUserParams from './parameters/SignInUserParams'; import type UpdateBeneficialOwnersForBankAccountParams from './parameters/UpdateBeneficialOwnersForBankAccountParams'; -type ApiRequest = ValueOf; +type ApiRequestType = ValueOf; const WRITE_COMMANDS = { SET_WORKSPACE_AUTO_REPORTING: 'SetWorkspaceAutoReporting', @@ -555,10 +555,10 @@ type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameter export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS}; type ApiCommand = WriteCommand | ReadCommand | SideEffectRequestCommand; -type CommandForRequestType = TRequestType extends typeof CONST.API_REQUEST_TYPE.WRITE +type CommandOfType = TRequestType extends typeof CONST.API_REQUEST_TYPE.WRITE ? WriteCommand : TRequestType extends typeof CONST.API_REQUEST_TYPE.READ ? ReadCommand : SideEffectRequestCommand; -export type {ApiCommand, ApiRequest, ApiRequestCommandParameters, CommandForRequestType, WriteCommand, ReadCommand, SideEffectRequestCommand}; +export type {ApiCommand, ApiRequestType, ApiRequestCommandParameters, CommandOfType, WriteCommand, ReadCommand, SideEffectRequestCommand}; From b0cf310a6c83bcb9452b67908634117d9ab7abe7 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 27 May 2024 15:55:59 -0700 Subject: [PATCH 018/512] Update existing usages of API.paginate --- src/libs/actions/Report.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e7c4d02be085..ccdd4df172b9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1078,15 +1078,18 @@ function getOlderActions(reportID: string, reportActionID: string) { }; API.paginate( + CONST.API_REQUEST_TYPE.READ, READ_COMMANDS.GET_OLDER_ACTIONS, parameters, - ONYXKEYS.COLLECTION.REPORT_ACTIONS, - ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, - (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - (reportAction) => reportAction.reportActionID, - false, {optimisticData, successData, failureData}, + { + resourceKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + pageKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + getItemID: (reportAction) => reportAction.reportActionID, + isInitialRequest: false, + }, ); } @@ -1133,15 +1136,18 @@ function getNewerActions(reportID: string, reportActionID: string) { }; API.paginate( + CONST.API_REQUEST_TYPE.READ, READ_COMMANDS.GET_NEWER_ACTIONS, parameters, - ONYXKEYS.COLLECTION.REPORT_ACTIONS, - ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, - (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - (reportAction) => reportAction.reportActionID, - false, {optimisticData, successData, failureData}, + { + resourceKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + pageKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + getItemID: (reportAction) => reportAction.reportActionID, + isInitialRequest: false, + }, ); } From 8ca521ea799fb49f15a31b847aa73a8652702538 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 27 May 2024 16:10:32 -0700 Subject: [PATCH 019/512] Add API.paginate to openReport --- src/libs/API/index.ts | 1 + src/libs/actions/Report.ts | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 080ea7ae1db1..806852cd4e31 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -231,3 +231,4 @@ function paginate = { + resourceKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + pageKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + getItemID: (reportAction) => reportAction.reportActionID, + isInitialRequest: true, + }; + if (isFromDeepLink) { - // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}).finally(() => { + API.paginate( + CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, + SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, + parameters, + {optimisticData, successData, failureData}, + paginationConfig, + ).finally(() => { Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); }); } else { // eslint-disable-next-line rulesdir/no-multiple-api-calls - API.write(WRITE_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}); + API.paginate(CONST.API_REQUEST_TYPE.WRITE, WRITE_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}, paginationConfig); } } From 962d006ae977ed8252825351b7fba973e4d01187 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 07:23:58 -0700 Subject: [PATCH 020/512] Upgrade typescript for ConditionalKeys fix --- package-lock.json | 20 ++++---------------- package.json | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 468190cb799f..64b187399887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -240,7 +240,7 @@ "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "type-fest": "^4.10.2", + "type-fest": "4.18.3", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", @@ -1855,19 +1855,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-assign": { - "version": "7.18.6", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", @@ -35997,9 +35984,10 @@ } }, "node_modules/type-fest": { - "version": "4.10.3", + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.3.tgz", + "integrity": "sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index f8eb679a2033..e0a8105d51e4 100644 --- a/package.json +++ b/package.json @@ -292,7 +292,7 @@ "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "type-fest": "^4.10.2", + "type-fest": "4.18.3", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", From 1b70ee960f5f1079b6836b9dbe6670e71f92e200 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 07:27:16 -0700 Subject: [PATCH 021/512] Remove duplicate comment --- src/libs/API/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 806852cd4e31..d34e5bb8fbc7 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -155,9 +155,6 @@ function makeRequestWithSideEffects( * write requests that use the same Onyx keys and haven't responded yet. */ function validateReadyToRead(command: TCommand) { - // Ensure all write requests on the sequential queue have finished responding before running read requests. - // Responses from read requests can overwrite the optimistic data inserted by - // write requests that use the same Onyx keys and haven't responded yet. if (PersistedRequests.getLength() > 0) { Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`); } From 4afecc6c623cc13465e3bc3db86587ef7bba5e2a Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 07:38:20 -0700 Subject: [PATCH 022/512] Fix CommandOfType utility --- src/libs/API/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c2e9d21c33f7..0b9e51c05fe3 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -555,7 +555,7 @@ type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameter export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS}; type ApiCommand = WriteCommand | ReadCommand | SideEffectRequestCommand; -type CommandOfType = TRequestType extends typeof CONST.API_REQUEST_TYPE.WRITE +type CommandOfType = TRequestType extends typeof CONST.API_REQUEST_TYPE.WRITE ? WriteCommand : TRequestType extends typeof CONST.API_REQUEST_TYPE.READ ? ReadCommand From f317885b5c9c8cdffb0264a95e028457428e564f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 08:06:34 -0700 Subject: [PATCH 023/512] Fix generic type default in PaginatedRequest --- src/libs/Middleware/Pagination.ts | 3 +-- src/libs/Middleware/types.ts | 5 +++-- src/types/onyx/Request.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 7c692812b4bb..3af23e796116 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -41,13 +41,12 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - // TODO: Provide key for the resource in the paginatedRequest. Also, we can probably derive the "pages" key from that resource. + // TODO: we can probably derive the pageKey from the resourceKey. const existingItems = OnyxCache.getValue(resourceKey) as OnyxValues[typeof resourceKey]; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); const existingPages = OnyxCache.getValue(pageKey) as Pages; - // TODO: generalize this function const mergedPages = PaginationUtils.mergeContinuousPages(sortedAllItems, [...existingPages, newPage], getItemID); response.onyxData.push({ diff --git a/src/libs/Middleware/types.ts b/src/libs/Middleware/types.ts index 148e9f361234..ea534326e38c 100644 --- a/src/libs/Middleware/types.ts +++ b/src/libs/Middleware/types.ts @@ -1,10 +1,11 @@ +import type {OnyxCollectionKey, OnyxPagesKey} from '@src/ONYXKEYS'; import type Request from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -type Middleware = ( +type Middleware = ( response: Promise, - request: Request | PaginatedRequest, + request: Request | PaginatedRequest, isFromSequentialQueue: boolean, ) => Promise; diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 07a9780b541d..6730781d02b0 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -26,7 +26,7 @@ type RequestData = { }; type Request = RequestData & OnyxData; -type PaginatedRequest = Request & { +type PaginatedRequest = Request & { isPaginated: true; resourceKey: TResourceKey; pageKey: TPageKey; From 90aecb38211213264e33ecb2a2bb0f919592b7a5 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 08:29:24 -0700 Subject: [PATCH 024/512] Move pusherSocketID from enhanceParameters back to API/index.js --- src/libs/API/index.ts | 7 +++++++ src/libs/Network/enhanceParameters.ts | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index d34e5bb8fbc7..0d0a40827651 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -4,6 +4,7 @@ import type {SetRequired} from 'type-fest'; import Log from '@libs/Log'; import * as Middleware from '@libs/Middleware'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import * as Pusher from '@libs/Pusher/pusher'; import * as Request from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; @@ -54,10 +55,16 @@ function prepareRequest(command: TCommand, type: Ap Onyx.update(optimisticData); } + const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE; + // Prepare the data we'll send to the API const data = { ...params, apiRequestType: type, + + // We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event + // is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775. + pusherSocketID: isWriteRequest ? Pusher.getPusherSocketID() : undefined, }; // Assemble all request metadata (used by middlewares, and for persisted requests stored in Onyx) diff --git a/src/libs/Network/enhanceParameters.ts b/src/libs/Network/enhanceParameters.ts index 2bebb0470296..f7fb857fba29 100644 --- a/src/libs/Network/enhanceParameters.ts +++ b/src/libs/Network/enhanceParameters.ts @@ -1,6 +1,5 @@ import * as Environment from '@libs/Environment/Environment'; import getPlatform from '@libs/getPlatform'; -import * as Pusher from '@libs/Pusher/pusher'; import CONFIG from '@src/CONFIG'; import {version as pkgVersion} from '../../../package.json'; import * as NetworkStore from './NetworkStore'; @@ -38,11 +37,6 @@ export default function enhanceParameters(command: string, parameters: Record Date: Tue, 28 May 2024 08:48:31 -0700 Subject: [PATCH 025/512] Consolidate PaginationConfig type --- src/libs/API/index.ts | 14 ++------------ src/libs/actions/Report.ts | 2 +- src/types/onyx/Request.ts | 10 +++++++--- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 0d0a40827651..972e66e173f6 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -8,9 +8,9 @@ import * as Pusher from '@libs/Pusher/pusher'; import * as Request from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; -import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; +import type {OnyxCollectionKey, OnyxPagesKey} from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; -import type {PaginatedRequest} from '@src/types/onyx/Request'; +import type {PaginatedRequest, PaginationConfig} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; @@ -190,15 +190,6 @@ function read(command: TCommand, apiCommandParamet }); } -type PaginationConfig = { - resourceKey: TResourceKey; - pageKey: TPageKey; - getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey]; - sortItems: (items: OnyxValues[TResourceKey]) => TResource[]; - getItemID: (item: TResource) => string; - isInitialRequest: boolean; -}; - function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( type: TRequestType, command: TCommand, @@ -235,4 +226,3 @@ function paginate = Request & { - isPaginated: true; + +type PaginationConfig = { resourceKey: TResourceKey; pageKey: TPageKey; getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey]; @@ -35,6 +35,10 @@ type PaginatedRequest string; isInitialRequest: boolean; }; +type PaginatedRequest = Request & + PaginationConfig & { + isPaginated: true; + }; export default Request; -export type {OnyxData, RequestType, PaginatedRequest}; +export type {OnyxData, RequestType, PaginationConfig, PaginatedRequest}; From 3326faebe0b5daad3f49b170e0cca3524b88d52e Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 11:31:16 -0700 Subject: [PATCH 026/512] Misc cleanup --- src/libs/API/index.ts | 2 +- src/libs/Middleware/Pagination.ts | 1 - src/libs/Middleware/types.ts | 7 +------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 972e66e173f6..9825e2f22caa 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -75,7 +75,7 @@ function prepareRequest(command: TCommand, type: Ap }; // This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650 - if (type === CONST.API_REQUEST_TYPE.WRITE) { + if (isWriteRequest) { request.data.shouldRetry = true; request.data.canCancel = true; } diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 3af23e796116..789e61a477b3 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -41,7 +41,6 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - // TODO: we can probably derive the pageKey from the resourceKey. const existingItems = OnyxCache.getValue(resourceKey) as OnyxValues[typeof resourceKey]; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); diff --git a/src/libs/Middleware/types.ts b/src/libs/Middleware/types.ts index ea534326e38c..3334e360e083 100644 --- a/src/libs/Middleware/types.ts +++ b/src/libs/Middleware/types.ts @@ -1,12 +1,7 @@ -import type {OnyxCollectionKey, OnyxPagesKey} from '@src/ONYXKEYS'; import type Request from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -type Middleware = ( - response: Promise, - request: Request | PaginatedRequest, - isFromSequentialQueue: boolean, -) => Promise; +type Middleware = (response: Promise, request: Request | PaginatedRequest, isFromSequentialQueue: boolean) => Promise; export default Middleware; From c8b2f509211c24f16949772789c2edf33490ca0b Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 15:03:57 -0700 Subject: [PATCH 027/512] Add getContinuousChain in PaginationUtils --- src/libs/PaginationUtils.ts | 114 ++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 6490c7a86b96..12059dbe2a12 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -9,24 +9,74 @@ type PageWithIndex = { lastIndex: number; }; +/** + * Finds the id and index in sortedItems of the first item in the given page that's present in sortedItems. + */ +function findFirstItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): {id: string; index: number} | null { + for (const id of page) { + if (id === CONST.PAGINATION_START_ID) { + return {id, index: 0}; + } + const index = sortedItems.findIndex((item) => getID(item) === id); + if (index === -1) { + return {id, index}; + } + } + return null; +} + +/** + * Finds the id and index in sortedItems of the last item in the given page that's present in sortedItems. + */ +function findLastItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): {id: string; index: number} | null { + for (const id of page.reverse()) { + if (id === CONST.PAGINATION_END_ID) { + return {id, index: sortedItems.length - 1}; + } + const index = sortedItems.findIndex((item) => getID(item) === id); + if (index !== -1) { + return {id, index}; + } + } + return null; +} + function getPagesWithIndexes(sortedItems: TResource[], pages: Pages, getID: (item: TResource) => string): PageWithIndex[] { - return pages.map((page) => { - const firstID = page[0]; - const lastID = page[page.length - 1]; - return { - ids: page, - firstID, - // TODO: What if the ID is not in the list? Could happen if an action is deleted from another device. - firstIndex: firstID === CONST.PAGINATION_START_ID ? 0 : sortedItems.findIndex((item) => getID(item) === firstID), - lastID, - lastIndex: lastID === CONST.PAGINATION_END_ID ? sortedItems.length - 1 : sortedItems.findIndex((item) => getID(item) === lastID), - }; - }); + return pages + .map((page) => { + let firstItem = findFirstItem(sortedItems, page, getID); + let lastItem = findLastItem(sortedItems, page, getID); + + // If all actions in the page are not found it will be removed. + if (firstItem === null || lastItem === null) { + return null; + } + + // In case actions were reordered, we need to swap them. + if (firstItem.index > lastItem.index) { + const temp = firstItem; + firstItem = lastItem; + lastItem = temp; + } + + return { + ids: sortedItems.slice(firstItem.index, lastItem.index + 1).map((item) => getID(item)), + firstID: firstItem.id, + firstIndex: firstItem.index, + lastID: lastItem.id, + lastIndex: lastItem.index, + }; + }) + .filter((page): page is PageWithIndex => page !== null); } function mergeContinuousPages(sortedItems: TResource[], pages: Pages, getItemID: (item: TResource) => string): Pages { const pagesWithIndexes = getPagesWithIndexes(sortedItems, pages, getItemID); + if (pagesWithIndexes.length === 0) { + return []; + } + // Pages need to be sorted by firstIndex ascending then by lastIndex descending const sortedPages = pagesWithIndexes.sort((a, b) => { if (a.firstIndex !== b.firstIndex) { @@ -68,4 +118,42 @@ function mergeContinuousPages(sortedItems: TResource[], pages: Pages, return result.map((page) => page.ids); } -export default {mergeContinuousPages}; +/** + * Returns the page of items that contains the item with the given ID, or the first page if null. + * See unit tests for example of inputs and expected outputs. + * + * Note: sortedItems should be sorted in descending order. + */ +function getContinuousChain(sortedItems: TResource[], pages: Pages, getID: (item: TResource) => string, id?: string): TResource[] { + if (pages.length === 0) { + return id ? [] : sortedItems; + } + + const pagesWithIndexes = getPagesWithIndexes(sortedItems, pages, getID); + + let page: PageWithIndex; + + if (id) { + const index = sortedItems.findIndex((item) => getID(item) === id); + + // If we are linking to an action that doesn't exist in Onyx, return an empty array + if (index === -1) { + return []; + } + + const linkedPage = pagesWithIndexes.find((pageIndex) => index >= pageIndex.firstIndex && index <= pageIndex.lastIndex); + + // If we are linked to an action in a gap return it by itself + if (!linkedPage) { + return [sortedItems[index]]; + } + + page = linkedPage; + } else { + page = pagesWithIndexes[0]; + } + + return sortedItems.slice(page.firstIndex, page.lastIndex + 1); +} + +export default {mergeContinuousPages, getContinuousChain}; From 4561bebbfaa0358d6981f4d59b8cb0047d643399 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 17:00:58 -0700 Subject: [PATCH 028/512] Save draft state - moving getContinuousChain to PaginationUtils --- src/libs/ReportActionsUtils.ts | 161 ------------------------------ tests/unit/PaginationUtilsTest.ts | 61 +++++++++++ 2 files changed, 61 insertions(+), 161 deletions(-) create mode 100644 tests/unit/PaginationUtilsTest.ts diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 49bea4dc3506..c37f46b61f4e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -7,7 +7,6 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActionsPages} from '@src/types/onyx'; import type { ActionName, ChangeLog, @@ -305,164 +304,6 @@ function getCombinedReportActions(reportActions: ReportAction[], transactionThre return getSortedReportActions(filteredReportActions, true); } -type PageWithIndexes = { - ids: string[]; - firstReportActionID: string; - firstReportActionIndex: number; - lastReportActionID: string; - lastReportActionIndex: number; -}; - -/** - * Finds the id, index in sortedReportActions and index in the page of the first valid report action in the given page. - */ -function findFirstReportAction(sortedReportActions: ReportAction[], page: string[]): {id: string; index: number} | null { - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < page.length; i++) { - const id = page[i]; - if (id === CONST.PAGINATION_START_ID) { - return {id, index: 0}; - } - const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); - if (index !== -1) { - return {id, index}; - } - } - return null; -} - -/** - * Finds the id, index in sortedReportActions and index in the page of the last valid report action in the given page. - */ -function findLastReportAction(sortedReportActions: ReportAction[], page: string[]): {id: string; index: number} | null { - for (let i = page.length - 1; i >= 0; i--) { - const id = page[i]; - if (id === CONST.PAGINATION_END_ID) { - return {id, index: sortedReportActions.length - 1}; - } - const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); - if (index !== -1) { - return {id, index}; - } - } - return null; -} - -function getPagesWithIndexes(sortedReportActions: ReportAction[], pages: ReportActionsPages): PageWithIndexes[] { - return pages - .map((page) => { - let firstReportAction = findFirstReportAction(sortedReportActions, page); - let lastReportAction = findLastReportAction(sortedReportActions, page); - - // If all actions in the page are not found it will be removed. - if (firstReportAction === null || lastReportAction === null) { - return null; - } - - // In case actions were reordered, we need to swap them. - if (firstReportAction.index > lastReportAction.index) { - const temp = firstReportAction; - firstReportAction = lastReportAction; - lastReportAction = temp; - } - - return { - ids: sortedReportActions.slice(firstReportAction.index, lastReportAction.index + 1).map((reportAction) => reportAction.reportActionID), - firstReportActionID: firstReportAction.id, - firstReportActionIndex: firstReportAction.index, - lastReportActionID: lastReportAction.id, - lastReportActionIndex: lastReportAction.index, - }; - }) - .filter((page) => page !== null) as PageWithIndexes[]; -} - -function mergeContinuousPages(sortedReportActions: ReportAction[], pages: ReportActionsPages): ReportActionsPages { - const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); - - if (pagesWithIndexes.length === 0) { - return []; - } - - // Pages need to be sorted by firstReportActionIndex ascending then by lastReportActionIndex descending. - const sortedPages = pagesWithIndexes.sort((a, b) => { - if (a.firstReportActionIndex !== b.firstReportActionIndex) { - return a.firstReportActionIndex - b.firstReportActionIndex; - } - return b.lastReportActionIndex - a.lastReportActionIndex; - }); - - const result = [sortedPages[0]]; - for (let i = 1; i < sortedPages.length; i++) { - const page = sortedPages[i]; - const prevPage = sortedPages[i - 1]; - - // Current page in inside the previous page, skip. - if (page.lastReportActionIndex <= prevPage.lastReportActionIndex) { - // eslint-disable-next-line no-continue - continue; - } - - // Current page is continuous with the previous page, merge. - // This happens if the ids from the current page and previous page are the same - // or if the indexes overlap. - if (page.firstReportActionID === prevPage.lastReportActionID || page.firstReportActionIndex < prevPage.lastReportActionIndex) { - result[result.length - 1] = { - firstReportActionID: prevPage.firstReportActionID, - firstReportActionIndex: prevPage.firstReportActionIndex, - lastReportActionID: page.lastReportActionID, - lastReportActionIndex: page.lastReportActionIndex, - // Only add items from prevPage that are not included in page in case of overlap. - ids: prevPage.ids.slice(0, prevPage.ids.indexOf(page.firstReportActionID)).concat(page.ids), - }; - // eslint-disable-next-line no-continue - continue; - } - - // No overlap, add the current page as is. - result.push(page); - } - - return result.map((page) => page.ids); -} - -/** - * Returns the page of actions that contains the given reportActionID, or the first page if null. - * See unit tests for example of inputs and expected outputs. - * Note: sortedReportActions sorted in descending order - */ -function getContinuousReportActionChain(sortedReportActions: ReportAction[], pages: ReportActionsPages, id?: string): ReportAction[] { - if (pages.length === 0) { - return id ? [] : sortedReportActions; - } - - const pagesWithIndexes = getPagesWithIndexes(sortedReportActions, pages); - - let page: PageWithIndexes; - - if (id) { - const index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); - - // If we are linking to an action that doesn't exist in onyx, return an empty array. - if (index === -1) { - return []; - } - - const linkedPage = pagesWithIndexes.find((pageIndex) => index >= pageIndex.firstReportActionIndex && index <= pageIndex.lastReportActionIndex); - - // If we are linking to an action in a gap just return it. - if (!linkedPage) { - return [sortedReportActions[index]]; - } - - page = linkedPage; - } else { - page = pagesWithIndexes[0]; - } - - return sortedReportActions.slice(page.firstReportActionIndex, page.lastReportActionIndex + 1); -} - /** * Finds most recent IOU request action ID. */ @@ -1339,7 +1180,6 @@ export { shouldReportActionBeVisible, shouldHideNewMarker, shouldReportActionBeVisibleAsLastAction, - getContinuousReportActionChain, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, isMemberChangeAction, @@ -1356,7 +1196,6 @@ export { isActionableJoinRequestPending, isActionableTrackExpense, isLinkedTransactionHeld, - mergeContinuousPages, }; export type {LastVisibleMessage}; diff --git a/tests/unit/PaginationUtilsTest.ts b/tests/unit/PaginationUtilsTest.ts new file mode 100644 index 000000000000..dfbe6b9af186 --- /dev/null +++ b/tests/unit/PaginationUtilsTest.ts @@ -0,0 +1,61 @@ +import {Pages} from '@src/types/onyx'; +import PaginationUtils from '../../src/libs/PaginationUtils'; + +type Item = { + id: string; +}; + +function createItems(ids: string[]): Item[] { + return ids.map((id) => ({ + id, + })); +} + +describe('PaginationUtils', () => { + describe('getContinuousChain', () => { + test.each([ + [ + ['1', '2', '3', '4', '5', '6', '7'], + ['7', '6', '5', '4', '3', '2', '1'], + ], + [ + ['9', '10', '11', '12'], + ['12', '11', '10', '9'], + ], + [ + ['14', '15', '16', '17'], + ['17', '16', '15', '14'], + ], + ])('given ID in the range %s, it will return the items with ID in range %s', (targetIDs, expectedOutputIDs) => { + const expectedOutput = createItems(expectedOutputIDs); + const input = createItems([ + '17', + '16', + '15', + '14', + // Gap + '12', + '11', + '10', + '9', + // Gap + '7', + '6', + '5', + '4', + '3', + '2', + '1', + ]); + const pages = [ + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], + ]; + for (const targetID of targetIDs) { + const result = PaginationUtils.getContinuousChain(input, pages, (item) => item.id, targetID); + expect(result).toStrictEqual(expectedOutput); + } + }); + }); +}); From f785b7e1e25e4e95a2912c7110e07c781ecd4c04 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 28 May 2024 17:27:45 -0700 Subject: [PATCH 029/512] Add a comment explaining the Pagination middleware --- src/libs/Middleware/Pagination.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 789e61a477b3..a55e901fe137 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -16,6 +16,17 @@ function isPaginatedRequest { if (!isPaginatedRequest(request)) { return requestResponse; From 800c2d6221bfdc55cd91528b5fa6ffe586dcb073 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 29 May 2024 13:18:10 +0800 Subject: [PATCH 030/512] shows a different message when approving all hold expenses --- src/components/ProcessMoneyReportHoldMenu.tsx | 16 ++++++++++++++-- src/languages/en.ts | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 6e81c9d57bc8..e5240b6997c0 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; @@ -64,12 +65,23 @@ function ProcessMoneyReportHoldMenu({ onClose(); }; + const promptText = useMemo(() => { + let promptTranslation: TranslationPaths; + if (nonHeldAmount) { + promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; + } else { + promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAmount'; + } + + return translate(promptTranslation); + }, [nonHeldAmount]); + return ( onSubmit(false)} diff --git a/src/languages/en.ts b/src/languages/en.ts index 0f5822b9f411..9122d9292a96 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -753,6 +753,7 @@ export default { expenseOnHold: 'This expense was put on hold. Review the comments for next steps.', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", + confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", payOnly: 'Pay only', From c20540b1a15c81253e64e647a5579c608db95976 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 30 May 2024 08:43:31 -0700 Subject: [PATCH 031/512] fix package-lock.json --- package-lock.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4346f86b91a4..9e010c7dc734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36042,9 +36042,10 @@ } }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From e5038c148eecd711f9fd5b8fd22e532471cad66a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 30 May 2024 16:49:30 -0400 Subject: [PATCH 032/512] Use new PaginationUtils.getContinuousChain --- src/pages/home/ReportScreen.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b28ff88044b7..9de575e32160 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -31,6 +31,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import Timing from '@libs/actions/Timing'; import Navigation from '@libs/Navigation/Navigation'; import clearReportNotifications from '@libs/Notification/clearReportNotifications'; +import PaginationUtils from '@libs/PaginationUtils'; import Performance from '@libs/Performance'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -258,7 +259,7 @@ function ReportScreen({ if (!sortedAllReportActions.length) { return []; } - return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, pages ?? [], reportActionIDFromRoute); + return PaginationUtils.getContinuousChain(sortedAllReportActions, pages ?? [], (item) => item.reportActionID, reportActionIDFromRoute); }, [reportActionIDFromRoute, sortedAllReportActions, pages]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. From 27cf878106790ab63a63a6feb711c3fdeb9caa48 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 30 May 2024 17:49:32 -0400 Subject: [PATCH 033/512] Handle no cache entries --- src/libs/Middleware/Pagination.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index a55e901fe137..8b8d0049f7af 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -52,11 +52,11 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - const existingItems = OnyxCache.getValue(resourceKey) as OnyxValues[typeof resourceKey]; + const existingItems = (OnyxCache.getValue(resourceKey) ?? {}) as OnyxValues[typeof resourceKey]; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); - const existingPages = OnyxCache.getValue(pageKey) as Pages; + const existingPages = (OnyxCache.getValue(pageKey) ?? []) as Pages; const mergedPages = PaginationUtils.mergeContinuousPages(sortedAllItems, [...existingPages, newPage], getItemID); response.onyxData.push({ From 9229520889e84d7d54be9e9d7c24c8813a2a9204 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 23 May 2024 09:44:21 +0200 Subject: [PATCH 034/512] Support invoice paying as business --- src/components/MoneyReportHeader.tsx | 4 +- .../ReportActionItem/ReportPreview.tsx | 4 +- src/components/SettlementButton.tsx | 23 ++++++++++- src/libs/API/parameters/PayInvoiceParams.ts | 1 + src/libs/ReportUtils.ts | 5 +++ src/libs/actions/IOU.ts | 38 +++++++++++++------ src/libs/actions/Policy/Policy.ts | 2 +- 7 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index ec52a6158ad7..43b2ca6729ea 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -119,7 +119,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || hasScanningReceipt || (shouldShowAnyButton && shouldUseNarrowLayout); - const confirmPayment = (type?: PaymentMethodType | undefined) => { + const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type || !chatReport) { return; } @@ -128,7 +128,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) { setIsHoldMenuVisible(true); } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport); + IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4677563d204f..abdb963530de 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -280,12 +280,12 @@ function ReportPreview({ }; }, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]); - const confirmPayment = (paymentMethodType?: PaymentMethodType) => { + const confirmPayment = (paymentMethodType?: PaymentMethodType, payAsBusiness?: boolean) => { if (!paymentMethodType || !chatReport || !iouReport) { return; } if (ReportUtils.isInvoiceReport(iouReport)) { - IOU.payInvoice(paymentMethodType, chatReport, iouReport); + IOU.payInvoice(paymentMethodType, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(paymentMethodType, chatReport, iouReport); } diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index b6e2a753c829..c5eaa0dec336 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -1,14 +1,16 @@ import React, {useEffect, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; import * as PaymentMethods from '@userActions/PaymentMethods'; +import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -143,6 +145,9 @@ function SettlementButton({ }: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + + const primaryPolicy = useMemo(() => PolicyActions.getPrimaryPolicy(activePolicyID) ?? null, [activePolicyID]); useEffect(() => { PaymentMethods.openWalletPage(); @@ -216,6 +221,22 @@ function SettlementButton({ }, ], }); + + if (PolicyUtils.isPolicyAdmin(primaryPolicy)) { + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); + } } if (shouldShowApproveButton) { diff --git a/src/libs/API/parameters/PayInvoiceParams.ts b/src/libs/API/parameters/PayInvoiceParams.ts index 4c6633749adb..a6b9746d87bc 100644 --- a/src/libs/API/parameters/PayInvoiceParams.ts +++ b/src/libs/API/parameters/PayInvoiceParams.ts @@ -4,6 +4,7 @@ type PayInvoiceParams = { reportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + payAsBusiness: boolean; }; export default PayInvoiceParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7e99c60cb618..7f06ebad8601 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -905,6 +905,10 @@ function isInvoiceRoom(report: OnyxEntry | EmptyObject): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; } +function isIndividualInvoiceRoom(report: OnyxEntry): boolean { + return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; +} + function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return currentUserAccountID === report.invoiceReceiver.accountID; @@ -7065,6 +7069,7 @@ export { isCurrentUserInvoiceReceiver, isDraftReport, createDraftWorkspaceAndNavigateToConfirmationScreen, + isIndividualInvoiceRoom, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a98a9c315173..6e8d065fb625 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -285,6 +285,12 @@ Onyx.connect({ }, }); +let primaryPolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => (primaryPolicyID = value), +}); + /** * Find the report preview action from given chat report and iou report */ @@ -5850,6 +5856,7 @@ function getPayMoneyRequestParams( recipient: Participant, paymentMethodType: PaymentMethodType, full: boolean, + payAsBusiness?: boolean, ): PayMoneyRequestData { const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport); @@ -5884,19 +5891,27 @@ function getPayMoneyRequestParams( optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithExpensify: paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA}); } + const optimisticChatReport = { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastVisibleActionCreated: optimisticIOUReportAction.created, + hasOutstandingChildRequest: false, + iouReportID: null, + lastMessageText: optimisticIOUReportAction.message?.[0]?.text, + lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, + }; + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + optimisticChatReport.invoiceReceiver = { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: primaryPolicyID, + }; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastVisibleActionCreated: optimisticIOUReportAction.created, - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText: optimisticIOUReportAction.message?.[0]?.text, - lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, - }, + value: optimisticChatReport, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -6495,19 +6510,20 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R Navigation.dismissModalWithReport(chatReport); } -function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report) { +function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report, payAsBusiness = false) { const recipient = {accountID: invoiceReport.ownerAccountID}; const { optimisticData, successData, failureData, params: {reportActionID}, - } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true); + } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); const params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, paymentMethodType, + payAsBusiness, }; API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 4d918352ba91..f3d152d81c40 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -235,7 +235,7 @@ function getPolicy(policyID: string | undefined): Policy | EmptyObject { */ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); - const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '']; + const primaryPolicy: Policy | null | undefined = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; return primaryPolicy ?? activeAdminWorkspaces[0]; } From 7f6e6eb93d212a88964851efa876122a10549adb Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 24 May 2024 14:33:03 +0200 Subject: [PATCH 035/512] UI updates to support B2B invoices rooms --- .../ReportActionItem/ReportPreview.tsx | 12 +++- src/libs/ReportUtils.ts | 70 ++++++++++++++++++- .../home/report/ReportActionItemSingle.tsx | 20 +----- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index abdb963530de..36831e26291e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -125,6 +125,7 @@ function ReportPreview({ const iouSettled = ReportUtils.isSettled(iouReportID); const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); + const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); const isApproved = ReportUtils.isReportApproved(iouReport); @@ -205,7 +206,16 @@ function ReportPreview({ if (isScanning) { return translate('common.receipt'); } - let payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + + let payerOrApproverName; + if (isPolicyExpenseChat) { + payerOrApproverName = ReportUtils.getPolicyName(chatReport); + } else if (isInvoiceRoom) { + payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport); + } else { + payerOrApproverName = ReportUtils.getDisplayNameForParticipant(managerID, true); + } + if (isApproved) { return translate('iou.managerApproved', {manager: payerOrApproverName}); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7f06ebad8601..b2a18925c9b4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -914,6 +914,11 @@ function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { return currentUserAccountID === report.invoiceReceiver.accountID; } + if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS) { + const policy = PolicyUtils.getPolicy(report.invoiceReceiver.policyID); + return PolicyUtils.isPolicyAdmin(policy); + } + return false; } @@ -1855,6 +1860,45 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f return shouldUseShortForm ? shortName : longName; } +function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry, displayAllActors: boolean, isWorkspaceActor: boolean, actorAccountID?: number): Icon { + let secondaryAvatar: Icon; + + if (displayAllActors) { + if (!isIndividualInvoiceRoom(chatReport)) { + const secondaryPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : '-1'; + const secondaryPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${secondaryPolicyID}`]; + const avatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); + + secondaryAvatar = { + source: avatar, + type: CONST.ICON_TYPE_WORKSPACE, + name: secondaryPolicy?.name, + id: secondaryPolicyID, + }; + } else { + const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport(iouReport) ? iouReport?.managerID : iouReport?.ownerAccountID; + const secondaryUserAvatar = allPersonalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; + const secondaryDisplayName = getDisplayNameForParticipant(secondaryAccountId); + + secondaryAvatar = { + source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), + type: CONST.ICON_TYPE_AVATAR, + name: secondaryDisplayName ?? '', + id: secondaryAccountId, + }; + } + } else if (!isWorkspaceActor) { + const avatarIconIndex = chatReport?.isOwnPolicyExpenseChat || isPolicyExpenseChat(chatReport) ? 0 : 1; + const reportIcons = getIcons(chatReport, {}); + + secondaryAvatar = reportIcons[avatarIconIndex]; + } else { + secondaryAvatar = {name: '', source: '', type: 'avatar'}; + } + + return secondaryAvatar; +} + function getParticipantAccountIDs(reportID: string) { const report = getReport(reportID); if (!report || !report.participants) { @@ -2018,7 +2062,12 @@ function getIcons( } else { const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(report, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicy.id, + }); } } } @@ -2093,7 +2142,12 @@ function getIcons( const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(invoiceRoomReport, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicy.id, + }); } return icons; @@ -2531,7 +2585,16 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); - let payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; + let payerOrApproverName; + if (isExpenseReport(report)) { + payerOrApproverName = getPolicyName(report, false, policy); + } else if (isInvoiceReport(report)) { + const chatReport = getReport(report?.chatReportID); + payerOrApproverName = getInvoicePayerName(chatReport); + } else { + payerOrApproverName = getDisplayNameForParticipant(report?.managerID) ?? ''; + } + const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, amount: formattedAmount, @@ -7070,6 +7133,7 @@ export { isDraftReport, createDraftWorkspaceAndNavigateToConfirmationScreen, isIndividualInvoiceRoom, + getSecondaryAvatar, }; export type { diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index f71db06c2d44..e6a870879598 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -84,7 +84,7 @@ function ReportActionItemSingle({ const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && !!iouReport, [action?.actionName, iouReport]); const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? {}); const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); let avatarSource = avatar; @@ -107,32 +107,16 @@ function ReportActionItemSingle({ } // If this is a report preview, display names and avatars of both people involved - let secondaryAvatar: Icon; + const secondaryAvatar = ReportUtils.getSecondaryAvatar(report, iouReport ?? null, displayAllActors, isWorkspaceActor, actorAccountID); const primaryDisplayName = displayName; if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport ? iouReport?.managerID : iouReport?.ownerAccountID; - const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); if (!isInvoiceReport) { displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; } - - secondaryAvatar = { - source: secondaryUserAvatar, - type: CONST.ICON_TYPE_AVATAR, - name: secondaryDisplayName ?? '', - id: secondaryAccountId, - }; - } else if (!isWorkspaceActor) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; - const reportIcons = ReportUtils.getIcons(report, {}); - - secondaryAvatar = reportIcons[avatarIconIndex]; - } else { - secondaryAvatar = {name: '', source: '', type: 'avatar'}; } const icon = { source: avatarSource ?? FallbackAvatar, From 506482133b00fc16f1e9bf82f331b48e5bc6127d Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 4 Jun 2024 16:35:35 +0200 Subject: [PATCH 036/512] Minor improvements --- src/components/SettlementButton.tsx | 4 ++-- src/libs/ReportUtils.ts | 9 +++++---- src/pages/home/report/ReportActionItemSingle.tsx | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index c5eaa0dec336..62b0c2ee89ac 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -43,7 +43,7 @@ type SettlementButtonOnyxProps = { type SettlementButtonProps = SettlementButtonOnyxProps & { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType) => void; + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; /** The route to redirect if user does not have a payment method setup */ enablePaymentsRoute: EnablePaymentsRoute; @@ -278,7 +278,7 @@ function SettlementButton({ return ( onPress(paymentType)} enablePaymentsRoute={enablePaymentsRoute} addBankAccountRoute={addBankAccountRoute} addDebitCardRoute={addDebitCardRoute} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2a18925c9b4..68f1cf9c830a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1867,21 +1867,22 @@ function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry< if (!isIndividualInvoiceRoom(chatReport)) { const secondaryPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : '-1'; const secondaryPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${secondaryPolicyID}`]; - const avatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); + const secondaryPolicyAvatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); secondaryAvatar = { - source: avatar, + source: secondaryPolicyAvatar, type: CONST.ICON_TYPE_WORKSPACE, name: secondaryPolicy?.name, id: secondaryPolicyID, }; } else { + // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport(iouReport) ? iouReport?.managerID : iouReport?.ownerAccountID; - const secondaryUserAvatar = allPersonalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; + const secondaryUserAvatar = allPersonalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = getDisplayNameForParticipant(secondaryAccountId); secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), + source: secondaryUserAvatar, type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName ?? '', id: secondaryAccountId, diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index e6a870879598..eee4f570ef1f 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -23,7 +23,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; -import type {Icon} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import ReportActionItemDate from './ReportActionItemDate'; import ReportActionItemFragment from './ReportActionItemFragment'; From 74e0b2510b903952acfd601de4436e1dc52aebed Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 4 Jun 2024 16:59:50 +0200 Subject: [PATCH 037/512] Lint fix --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 68f1cf9c830a..2569bc56a331 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1889,7 +1889,7 @@ function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry< }; } } else if (!isWorkspaceActor) { - const avatarIconIndex = chatReport?.isOwnPolicyExpenseChat || isPolicyExpenseChat(chatReport) ? 0 : 1; + const avatarIconIndex = !!chatReport?.isOwnPolicyExpenseChat || isPolicyExpenseChat(chatReport) ? 0 : 1; const reportIcons = getIcons(chatReport, {}); secondaryAvatar = reportIcons[avatarIconIndex]; From 6e28975fcd764baaeefc1c3a0767f870c9a82020 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 5 Jun 2024 16:48:35 +0700 Subject: [PATCH 038/512] fix selected option is not highlighted --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index f3f7f56be44f..8edb51967cd5 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -157,7 +157,7 @@ function BaseSelectionList( itemLayouts.push({length: fullItemHeight, offset}); offset += fullItemHeight; - if (item.isSelected) { + if (item.isSelected && !selectedOptions.find((option) => option.text === item.text)) { selectedOptions.push(item); } }); From 2bc68fdf8123b527993a01fe473130d5f0afa88c Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 6 Jun 2024 16:53:08 -0400 Subject: [PATCH 039/512] Fixes after update main --- src/libs/Middleware/Pagination.ts | 3 ++- src/libs/PaginationUtils.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 8b8d0049f7af..ee660cb1b628 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -1,5 +1,5 @@ // TODO: Is this a legit use case for exposing `OnyxCache`, or should we use `Onyx.connect`? -import fastMerge from 'expensify-common/lib/fastMerge'; +import fastMerge from 'expensify-common/dist/fastMerge'; import Onyx from 'react-native-onyx'; import OnyxCache from 'react-native-onyx/dist/OnyxCache'; import Log from '@libs/Log'; @@ -28,6 +28,7 @@ function isPaginatedRequest { + console.log(isPaginatedRequest(request), request.command); if (!isPaginatedRequest(request)) { return requestResponse; } diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 12059dbe2a12..fc8f4e326412 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -153,7 +153,7 @@ function getContinuousChain(sortedItems: TResource[], pages: Pages, g page = pagesWithIndexes[0]; } - return sortedItems.slice(page.firstIndex, page.lastIndex + 1); + return page ? sortedItems.slice(page.firstIndex, page.lastIndex + 1) : sortedItems; } export default {mergeContinuousPages, getContinuousChain}; From 5428d9466e07f32f2e2cad212d71798d3028209b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 6 Jun 2024 17:45:55 -0400 Subject: [PATCH 040/512] Back in working state --- src/libs/Middleware/Pagination.ts | 1 - src/libs/PaginationUtils.ts | 3 +-- src/libs/actions/Report.ts | 12 ++++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index ee660cb1b628..f4e47bcf8c84 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -28,7 +28,6 @@ function isPaginatedRequest { - console.log(isPaginatedRequest(request), request.command); if (!isPaginatedRequest(request)) { return requestResponse; } diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index fc8f4e326412..cfd38f6f402b 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -18,7 +18,7 @@ function findFirstItem(sortedItems: TResource[], page: string[], getI return {id, index: 0}; } const index = sortedItems.findIndex((item) => getID(item) === id); - if (index === -1) { + if (index !== -1) { return {id, index}; } } @@ -72,7 +72,6 @@ function getPagesWithIndexes(sortedItems: TResource[], pages: Pages, function mergeContinuousPages(sortedItems: TResource[], pages: Pages, getItemID: (item: TResource) => string): Pages { const pagesWithIndexes = getPagesWithIndexes(sortedItems, pages, getItemID); - if (pagesWithIndexes.length === 0) { return []; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1d21e1a9dc84..7aab40fc19be 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -947,8 +947,8 @@ function openReport( parameters.clientLastReadTime = currentReportData?.[reportID]?.lastReadTime ?? ''; const paginationConfig: PaginationConfig = { - resourceKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - pageKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + resourceKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + pageKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, @@ -1118,8 +1118,8 @@ function getOlderActions(reportID: string, reportActionID: string) { parameters, {optimisticData, successData, failureData}, { - resourceKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - pageKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + resourceKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + pageKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, @@ -1176,8 +1176,8 @@ function getNewerActions(reportID: string, reportActionID: string) { parameters, {optimisticData, successData, failureData}, { - resourceKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - pageKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + resourceKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + pageKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, From 77eff746cf89ffb75a708a105476442c9b7deb2b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 6 Jun 2024 19:03:35 -0400 Subject: [PATCH 041/512] Ts fixes and minor api changes --- src/libs/API/index.ts | 10 +++++----- src/libs/Middleware/Pagination.ts | 15 +++++++++------ src/libs/Middleware/types.ts | 3 ++- src/libs/actions/Report.ts | 20 ++++++++++---------- src/types/onyx/Request.ts | 16 ++++++++-------- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 9825e2f22caa..ae3d78de4e32 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -190,22 +190,22 @@ function read(command: TCommand, apiCommandParamet }); } -function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( +function paginate, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData, - config: PaginationConfig, + config: PaginationConfig, ): TRequestType extends typeof CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS ? Promise : void; -function paginate, TResource, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( +function paginate, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData, - config: PaginationConfig, + config: PaginationConfig, ): Promise | void { Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters}); - const request: PaginatedRequest = { + const request: PaginatedRequest = { ...prepareRequest(command, type, apiCommandParameters, onyxData), ...config, ...{ diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index f4e47bcf8c84..4c4e30594494 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -10,9 +10,9 @@ import type {Pages, Request} from '@src/types/onyx'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Middleware from './types'; -function isPaginatedRequest( - request: Request | PaginatedRequest, -): request is PaginatedRequest { +function isPaginatedRequest( + request: Request | PaginatedRequest, +): request is PaginatedRequest { return 'isPaginated' in request && request.isPaginated; } @@ -32,14 +32,17 @@ const Pagination: Middleware = (requestResponse, request) => { return requestResponse; } - const {resourceKey, pageKey, getItemsFromResponse, sortItems, getItemID, isInitialRequest} = request; + const {resourceCollectionKey, resourceID, pageCollectionKey, sortItems, getItemID, isInitialRequest} = request; return requestResponse.then((response) => { if (!response?.onyxData) { return Promise.resolve(response); } + const resourceKey = `${resourceCollectionKey}${resourceID}` as const; + const pageKey = `${pageCollectionKey}${resourceID}` as const; + // Create a new page based on the response - const pageItems = getItemsFromResponse(response); + const pageItems = (response.onyxData.find((data) => data.key === resourceKey)?.value ?? {}) as OnyxValues[typeof resourceCollectionKey]; const sortedPageItems = sortItems(pageItems); if (sortedPageItems.length === 0) { // Must have at least 1 action to create a page. @@ -52,7 +55,7 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - const existingItems = (OnyxCache.getValue(resourceKey) ?? {}) as OnyxValues[typeof resourceKey]; + const existingItems = (OnyxCache.getValue(resourceKey) ?? {}) as OnyxValues[typeof resourceCollectionKey]; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); diff --git a/src/libs/Middleware/types.ts b/src/libs/Middleware/types.ts index 3334e360e083..9c9a48ac491d 100644 --- a/src/libs/Middleware/types.ts +++ b/src/libs/Middleware/types.ts @@ -1,7 +1,8 @@ +import type {OnyxCollectionKey, OnyxPagesKey} from '@src/ONYXKEYS'; import type Request from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -type Middleware = (response: Promise, request: Request | PaginatedRequest, isFromSequentialQueue: boolean) => Promise; +type Middleware = (response: Promise, request: Request | PaginatedRequest, isFromSequentialQueue: boolean) => Promise; export default Middleware; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7aab40fc19be..77a1cb256e0e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -946,10 +946,10 @@ function openReport( parameters.clientLastReadTime = currentReportData?.[reportID]?.lastReadTime ?? ''; - const paginationConfig: PaginationConfig = { - resourceKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - pageKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, - getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + const paginationConfig: PaginationConfig = { + resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + resourceID: reportID, + pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, isInitialRequest: true, @@ -1118,9 +1118,9 @@ function getOlderActions(reportID: string, reportActionID: string) { parameters, {optimisticData, successData, failureData}, { - resourceKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - pageKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, - getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + resourceID: reportID, + pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, isInitialRequest: false, @@ -1176,9 +1176,9 @@ function getNewerActions(reportID: string, reportActionID: string) { parameters, {optimisticData, successData, failureData}, { - resourceKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - pageKey: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportID}`, - getItemsFromResponse: (response) => response?.onyxData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)?.value as ReportActions, + resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + resourceID: reportID, + pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, isInitialRequest: false, diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 7a281b225978..2146b6b1b7e5 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -27,16 +27,16 @@ type RequestData = { type Request = RequestData & OnyxData; -type PaginationConfig = { - resourceKey: TResourceKey; - pageKey: TPageKey; - getItemsFromResponse: (response: Response) => OnyxValues[TResourceKey]; - sortItems: (items: OnyxValues[TResourceKey]) => TResource[]; - getItemID: (item: TResource) => string; +type PaginationConfig = { + resourceCollectionKey: TResourceKey; + resourceID: string; + pageCollectionKey: TPageKey; + sortItems: (items: OnyxValues[TResourceKey]) => Array ? TResource : never>; + getItemID: (item: OnyxValues[TResourceKey] extends Record ? TResource : never) => string; isInitialRequest: boolean; }; -type PaginatedRequest = Request & - PaginationConfig & { +type PaginatedRequest = Request & + PaginationConfig & { isPaginated: true; }; From 8dd4dfe578a7be1fb6ad2dde88b8151991904e83 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 6 Jun 2024 21:48:35 -0400 Subject: [PATCH 042/512] Fix most tests --- tests/unit/PaginationUtilsTest.ts | 433 +++++++++++++++++++- tests/unit/ReportActionsUtilsTest.ts | 572 +-------------------------- 2 files changed, 433 insertions(+), 572 deletions(-) diff --git a/tests/unit/PaginationUtilsTest.ts b/tests/unit/PaginationUtilsTest.ts index dfbe6b9af186..93f5f888ed06 100644 --- a/tests/unit/PaginationUtilsTest.ts +++ b/tests/unit/PaginationUtilsTest.ts @@ -1,4 +1,5 @@ -import {Pages} from '@src/types/onyx'; +import CONST from '@src/CONST'; +import type {Pages} from '@src/types/onyx'; import PaginationUtils from '../../src/libs/PaginationUtils'; type Item = { @@ -11,6 +12,10 @@ function createItems(ids: string[]): Item[] { })); } +function getID(item: Item) { + return item.id; +} + describe('PaginationUtils', () => { describe('getContinuousChain', () => { test.each([ @@ -53,9 +58,433 @@ describe('PaginationUtils', () => { ['7', '6', '5', '4', '3', '2', '1'], ]; for (const targetID of targetIDs) { - const result = PaginationUtils.getContinuousChain(input, pages, (item) => item.id, targetID); + const result = PaginationUtils.getContinuousChain(input, pages, getID, targetID); expect(result).toStrictEqual(expectedOutput); } }); + + it('given an input ID of 8 or 13 which do not exist in Onyx it will return an empty array', () => { + const input: Item[] = createItems([ + // Given these sortedItems + '17', + '16', + '15', + '14', + // Gap + '12', + '11', + '10', + '9', + // Gap + '7', + '6', + '5', + '4', + '3', + '2', + '1', + ]); + + const pages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], + ]; + + // Expect these sortedItems + const expectedResult: Item[] = []; + const result = PaginationUtils.getContinuousChain(input, pages, getID, '8'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an input ID of an action in a gap it will return only that action', () => { + const input = createItems([ + // Given these sortedItems + '17', + '16', + '15', + '14', + '13', + '12', + '11', + '10', + '9', + '8', + '7', + '6', + '5', + '4', + '3', + '2', + '1', + ]); + + const pages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2', '1'], + ]; + + const expectedResult = createItems([ + // Expect these sortedItems + '8', + ]); + const result = PaginationUtils.getContinuousChain(input, pages, getID, '8'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an empty input ID and the report only contains pending actions, it will return all actions', () => { + const input = createItems([ + // Given these sortedItems + '7', + '6', + '5', + '4', + '3', + '2', + '1', + ]); + + const pages: Pages = []; + + // Expect these sortedItems + const expectedResult = [...input]; + const result = PaginationUtils.getContinuousChain(input, pages, getID, ''); + expect(result).toStrictEqual(expectedResult); + }); + + it('given an empty input ID and the report only contains pending actions, it will return an empty array', () => { + const input = createItems([ + // Given these sortedItems + '7', + '6', + '5', + '4', + '3', + '2', + '1', + ]); + + const pages: Pages = []; + + // Expect these sortedItems + const expectedResult: Item[] = []; + const result = PaginationUtils.getContinuousChain(input, pages, getID, '4'); + expect(result).toStrictEqual(expectedResult); + }); + + it('does not include actions outside of pages', () => { + const input = createItems([ + // Given these sortedItems + '17', + '16', + '15', + '14', + '13', + '12', + '11', + '10', + '9', + '8', + '7', + '6', + '5', + '4', + '3', + '2', + '1', + ]); + + const pages = [ + // Given these pages + ['17', '16', '15', '14'], + ['12', '11', '10', '9'], + ['7', '6', '5', '4', '3', '2'], + ]; + + const expectedResult = createItems([ + // Expect these sortedItems + '12', + '11', + '10', + '9', + ]); + const result = PaginationUtils.getContinuousChain(input, pages, getID, '10'); + expect(result).toStrictEqual(expectedResult); + }); + + it('given a page with null firstItemID include actions from the start', () => { + const input = createItems([ + // Given these sortedItems + '17', + '16', + '15', + '14', + ]); + + const pages = [ + // Given these pages + [CONST.PAGINATION_START_ID, '15', '14'], + ]; + + const expectedResult = createItems([ + // Expect these sortedItems + '17', + '16', + '15', + '14', + ]); + const result = PaginationUtils.getContinuousChain(input, pages, getID, ''); + expect(result).toStrictEqual(expectedResult); + }); + + it('given a page with null lastItemID include actions to the end', () => { + const input = createItems([ + // Given these sortedItems + '17', + '16', + '15', + '14', + ]); + + const pages = [ + // Given these pages + ['17', '16', CONST.PAGINATION_END_ID], + ]; + + const expectedResult = createItems([ + // Expect these sortedItems + '17', + '16', + '15', + '14', + ]); + const result = PaginationUtils.getContinuousChain(input, pages, getID, ''); + expect(result).toStrictEqual(expectedResult); + }); + }); + + describe('mergeContinuousPages', () => { + it('merges continuous pages', () => { + const sortedItems = createItems([ + // Given these sortedItems + '5', + '4', + '3', + '2', + '1', + ]); + const pages = [ + // Given these pages + ['5', '4', '3'], + ['3', '2', '1'], + ]; + const expectedResult = [ + // Expect these pages + ['5', '4', '3', '2', '1'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('merges overlapping pages', () => { + const sortedItems = createItems([ + // Given these sortedItems + '5', + '4', + '3', + '2', + '1', + ]); + const pages = [ + // Given these pages + ['4', '3', '2'], + ['3', '2', '1'], + ]; + const expectedResult = [ + // Expect these pages + ['4', '3', '2', '1'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('merges included pages', () => { + const sortedItems = createItems([ + // Given these sortedItems + '5', + '4', + '3', + '2', + '1', + ]); + const pages = [ + // Given these pages + ['5', '4', '3', '2', '1'], + ['5', '4', '3', '2'], + ]; + const expectedResult = [ + // Expect these pages + ['5', '4', '3', '2', '1'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('do not merge separate pages', () => { + const sortedItems = createItems([ + // Given these sortedItems + '5', + '4', + // Gap + '2', + '1', + ]); + const pages = [ + // Given these pages + ['5', '4'], + ['2', '1'], + ]; + const expectedResult = [ + // Expect these pages + ['5', '4'], + ['2', '1'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('sorts pages', () => { + const sortedItems = createItems([ + // Given these sortedItems + '9', + '8', + // Gap + '6', + '5', + // Gap + '3', + '2', + '1', + ]); + const pages = [ + // Given these pages + ['3', '2', '1'], + ['3', '2'], + ['6', '5'], + ['9', '8'], + ]; + const expectedResult = [ + // Expect these pages + ['9', '8'], + ['6', '5'], + ['3', '2', '1'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles actions that no longer exist', () => { + const sortedItems = createItems([ + // Given these sortedItems + '4', + '3', + ]); + const pages = [ + // Given these pages + ['6', '5', '4', '3', '2', '1'], + ]; + const expectedResult = [ + // Expect these pages + ['4', '3'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('removes pages that are empty', () => { + const sortedItems = createItems([ + // Given these sortedItems + '4', + ]); + const pages = [ + // Given these pages + ['6', '5'], + ['3', '2', '1'], + ]; + + // Expect these pages + const expectedResult: Pages = []; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles pages with a single action', () => { + const sortedItems = createItems([ + // Given these sortedItems + '4', + '2', + ]); + const pages = [ + // Given these pages + ['4'], + ['2'], + ['2'], + ]; + const expectedResult = [ + // Expect these pages + ['4'], + ['2'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles out of order ids', () => { + const sortedItems = createItems([ + // Given these sortedItems + '2', + '1', + '3', + '4', + ]); + const pages = [ + // Given these pages + ['2', '1'], + ['1', '3'], + ['4'], + ]; + const expectedResult = [ + // Expect these pages + ['2', '1', '3'], + ['4'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles basic reordering', () => { + const sortedItems = createItems([ + // Given these sortedItems + '1', + '2', + '4', + '5', + ]); + const pages = [ + // Given these pages + ['5', '4'], + ['2', '1'], + ]; + const expectedResult = [ + // Expect these pages + ['1', '2'], + ['4', '5'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); }); }); diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 63f7baed3b84..0254288d6df9 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1,33 +1,13 @@ -import Onyx from 'react-native-onyx'; import type {KeyValueMapping} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; -import type {Report, ReportAction, ReportActionsPages} from '../../src/types/onyx'; +import type {Report, ReportAction} from '../../src/types/onyx'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -function createReportAction(id: string) { - return { - reportActionID: id, - created: '2022-11-13 22:27:01.825', - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }; -} - describe('ReportActionsUtils', () => { beforeAll(() => Onyx.init({ @@ -462,554 +442,6 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); }); - describe('getContinuousReportActionChain', () => { - it('given an input ID of 1, ..., 7 it will return the report actions with id 1 - 7', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - // Gap - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - // Gap - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', '15', '14'], - ['12', '11', '10', '9'], - ['7', '6', '5', '4', '3', '2', '1'], - ]; - - const expectedResult = [ - // Expect these sortedReportActions - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '3'); - expect(result).toStrictEqual(expectedResult); - }); - - it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - // Gap - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - // Gap - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', '15', '14'], - ['12', '11', '10', '9'], - ['7', '6', '5', '4', '3', '2', '1'], - ]; - - const expectedResult = [ - // Expect these sortedReportActions - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '10'); - expect(result).toStrictEqual(expectedResult); - }); - - it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - // Gap - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - // Gap - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', '15', '14'], - ['12', '11', '10', '9'], - ['7', '6', '5', '4', '3', '2', '1'], - ]; - - const expectedResult = [ - // Expect these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '16'); - expect(result).toStrictEqual(expectedResult); - }); - - it('given an input ID of 8 or 13 which do not exist in Onyx it will return an empty array', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - // Gap - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - // Gap - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', '15', '14'], - ['12', '11', '10', '9'], - ['7', '6', '5', '4', '3', '2', '1'], - ]; - - // Expect these sortedReportActions - const expectedResult: ReportAction[] = []; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '8'); - expect(result).toStrictEqual(expectedResult); - }); - - it('given an input ID of an action in a gap it will return only that action', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - createReportAction('13'), - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - createReportAction('8'), - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', '15', '14'], - ['12', '11', '10', '9'], - ['7', '6', '5', '4', '3', '2', '1'], - ]; - - const expectedResult: ReportAction[] = [ - // Expect these sortedReportActions - createReportAction('8'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '8'); - expect(result).toStrictEqual(expectedResult); - }); - - it('given an empty input ID and the report only contains pending actions, it will return all actions', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = []; - - // Expect these sortedReportActions - const expectedResult = [...input]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, ''); - expect(result).toStrictEqual(expectedResult); - }); - - it('given an empty input ID and the report only contains pending actions, it will return an empty array', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = []; - - // Expect these sortedReportActions - const expectedResult: ReportAction[] = []; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '4'); - expect(result).toStrictEqual(expectedResult); - }); - - it('does not include actions outside of pages', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - createReportAction('13'), - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - createReportAction('8'), - createReportAction('7'), - createReportAction('6'), - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', '15', '14'], - ['12', '11', '10', '9'], - ['7', '6', '5', '4', '3', '2'], - ]; - - const expectedResult = [ - // Expect these sortedReportActions - createReportAction('12'), - createReportAction('11'), - createReportAction('10'), - createReportAction('9'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, '10'); - expect(result).toStrictEqual(expectedResult); - }); - - it('given a page with null firstReportActionID include actions from the start', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - [CONST.PAGINATION_START_ID, '15', '14'], - ]; - - const expectedResult = [ - // Expect these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, ''); - expect(result).toStrictEqual(expectedResult); - }); - - it('given a page with null lastReportActionID include actions to the end', () => { - const input: ReportAction[] = [ - // Given these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - ]; - - const pages: ReportActionsPages = [ - // Given these pages - ['17', '16', CONST.PAGINATION_END_ID], - ]; - - const expectedResult = [ - // Expect these sortedReportActions - createReportAction('17'), - createReportAction('16'), - createReportAction('15'), - createReportAction('14'), - ]; - const result = ReportActionsUtils.getContinuousReportActionChain(input, pages, ''); - expect(result).toStrictEqual(expectedResult); - }); - }); - - describe('mergeContinuousPages', () => { - it('merges continuous pages', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['5', '4', '3'], - ['3', '2', '1'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['5', '4', '3', '2', '1'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('merges overlapping pages', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['4', '3', '2'], - ['3', '2', '1'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['4', '3', '2', '1'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('merges included pages', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('5'), - createReportAction('4'), - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['5', '4', '3', '2', '1'], - ['5', '4', '3', '2'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['5', '4', '3', '2', '1'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('do not merge separate pages', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('5'), - createReportAction('4'), - // Gap - createReportAction('2'), - createReportAction('1'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['5', '4'], - ['2', '1'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['5', '4'], - ['2', '1'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('sorts pages', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('9'), - createReportAction('8'), - // Gap - createReportAction('6'), - createReportAction('5'), - // Gap - createReportAction('3'), - createReportAction('2'), - createReportAction('1'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['3', '2', '1'], - ['3', '2'], - ['6', '5'], - ['9', '8'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['9', '8'], - ['6', '5'], - ['3', '2', '1'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('handles actions that no longer exist', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('4'), - createReportAction('3'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['6', '5', '4', '3', '2', '1'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['4', '3'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('removes pages that are empty', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('4'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['6', '5'], - ['3', '2', '1'], - ]; - - // Expect these pages - const expectedResult: ReportActionsPages = []; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('handles pages with a single action', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('4'), - createReportAction('2'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['4'], - ['2'], - ['2'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['4'], - ['2'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('handles out of order ids', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('2'), - createReportAction('1'), - createReportAction('3'), - createReportAction('4'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['2', '1'], - ['1', '3'], - ['4'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['2', '1', '3'], - ['4'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - - it('handles basic reordering', () => { - const sortedReportActions = [ - // Given these sortedReportActions - createReportAction('1'), - createReportAction('2'), - createReportAction('4'), - createReportAction('5'), - ]; - const pages: ReportActionsPages = [ - // Given these pages - ['5', '4'], - ['2', '1'], - ]; - const expectedResult: ReportActionsPages = [ - // Expect these pages - ['1', '2'], - ['4', '5'], - ]; - const result = ReportActionsUtils.mergeContinuousPages(sortedReportActions, pages); - expect(result).toStrictEqual(expectedResult); - }); - }); describe('getLastVisibleAction', () => { it('should return the last visible action for a report', () => { From e908d64344ef3fb6b0d079aca6049a2da48ccd11 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 6 Jun 2024 22:27:39 -0400 Subject: [PATCH 043/512] Fix --- src/libs/PaginationUtils.ts | 12 ++++++++++-- src/libs/actions/Report.ts | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index cfd38f6f402b..969bdef8ff8c 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -29,7 +29,7 @@ function findFirstItem(sortedItems: TResource[], page: string[], getI * Finds the id and index in sortedItems of the last item in the given page that's present in sortedItems. */ function findLastItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): {id: string; index: number} | null { - for (const id of page.reverse()) { + for (const id of page.slice().reverse()) { if (id === CONST.PAGINATION_END_ID) { return {id, index: sortedItems.length - 1}; } @@ -59,8 +59,16 @@ function getPagesWithIndexes(sortedItems: TResource[], pages: Pages, lastItem = temp; } + const ids = sortedItems.slice(firstItem.index, lastItem.index + 1).map((item) => getID(item)); + if (firstItem.id === CONST.PAGINATION_START_ID) { + ids.unshift(CONST.PAGINATION_START_ID); + } + if (lastItem.id === CONST.PAGINATION_END_ID) { + ids.push(CONST.PAGINATION_END_ID); + } + return { - ids: sortedItems.slice(firstItem.index, lastItem.index + 1).map((item) => getID(item)), + ids, firstID: firstItem.id, firstIndex: firstItem.index, lastID: lastItem.id, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 77a1cb256e0e..732e3c2d473f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -952,7 +952,7 @@ function openReport( pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, - isInitialRequest: true, + isInitialRequest: !reportActionID, }; if (isFromDeepLink) { From 1a4ec8081758f997cee59cac34fd1f664d65c7ee Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 6 Jun 2024 23:21:04 -0400 Subject: [PATCH 044/512] Fix scrolling from linked message --- src/libs/Middleware/Pagination.ts | 7 +++++-- src/libs/PaginationUtils.ts | 6 ++++++ src/libs/actions/Report.ts | 6 +++--- src/types/onyx/Request.ts | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 4c4e30594494..4c6da850b2df 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -32,7 +32,7 @@ const Pagination: Middleware = (requestResponse, request) => { return requestResponse; } - const {resourceCollectionKey, resourceID, pageCollectionKey, sortItems, getItemID, isInitialRequest} = request; + const {resourceCollectionKey, resourceID, pageCollectionKey, sortItems, getItemID, requestType} = request; return requestResponse.then((response) => { if (!response?.onyxData) { return Promise.resolve(response); @@ -51,7 +51,10 @@ const Pagination: Middleware = (requestResponse, request) => { } const newPage = sortedPageItems.map((item) => getItemID(item)); - if (isInitialRequest) { + + // Detect if we are at the start of the list. This will always be the case for the initial request. + // For previous requests we check that no new data is returned. Ideally the server would return that info. + if (requestType === 'initial' || (requestType === 'next' && newPage.length === 1)) { newPage.unshift(CONST.PAGINATION_START_ID); } diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 969bdef8ff8c..89edafafc2f9 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -86,6 +86,12 @@ function mergeContinuousPages(sortedItems: TResource[], pages: Pages, // Pages need to be sorted by firstIndex ascending then by lastIndex descending const sortedPages = pagesWithIndexes.sort((a, b) => { + if (a.firstID === CONST.PAGINATION_START_ID) { + return -1; + } + if (a.lastID === CONST.PAGINATION_END_ID) { + return 1; + } if (a.firstIndex !== b.firstIndex) { return a.firstIndex - b.firstIndex; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 732e3c2d473f..eb6fd5baa21a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -952,7 +952,7 @@ function openReport( pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, - isInitialRequest: !reportActionID, + requestType: !reportActionID ? 'initial' : 'link', }; if (isFromDeepLink) { @@ -1123,7 +1123,7 @@ function getOlderActions(reportID: string, reportActionID: string) { pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, - isInitialRequest: false, + requestType: 'previous', }, ); } @@ -1181,7 +1181,7 @@ function getNewerActions(reportID: string, reportActionID: string) { pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, - isInitialRequest: false, + requestType: 'next', }, ); } diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 2146b6b1b7e5..780632020e71 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -33,7 +33,7 @@ type PaginationConfig Array ? TResource : never>; getItemID: (item: OnyxValues[TResourceKey] extends Record ? TResource : never) => string; - isInitialRequest: boolean; + requestType: 'initial' | 'link' | 'next' | 'previous'; }; type PaginatedRequest = Request & PaginationConfig & { From ba6358e73f7da310d413af53cff15cca172d9279 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 7 Jun 2024 00:54:15 -0400 Subject: [PATCH 045/512] Move config outside of request object --- src/libs/API/index.ts | 11 ++++---- src/libs/Middleware/Pagination.ts | 45 +++++++++++++++++++++++++------ src/libs/Middleware/index.ts | 2 +- src/libs/Middleware/types.ts | 3 +-- src/libs/actions/Report.ts | 32 +++++++++++----------- src/types/onyx/Request.ts | 14 ++++------ 6 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index ae3d78de4e32..575477d4220b 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -8,7 +8,6 @@ import * as Pusher from '@libs/Pusher/pusher'; import * as Request from '@libs/Request'; import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; -import type {OnyxCollectionKey, OnyxPagesKey} from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; import type {PaginatedRequest, PaginationConfig} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; @@ -190,22 +189,22 @@ function read(command: TCommand, apiCommandParamet }); } -function paginate, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( +function paginate>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData, - config: PaginationConfig, + config: PaginationConfig, ): TRequestType extends typeof CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS ? Promise : void; -function paginate, TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey>( +function paginate>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData, - config: PaginationConfig, + config: PaginationConfig, ): Promise | void { Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters}); - const request: PaginatedRequest = { + const request: PaginatedRequest = { ...prepareRequest(command, type, apiCommandParameters, onyxData), ...config, ...{ diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 4c6da850b2df..314db6643d89 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -2,6 +2,7 @@ import fastMerge from 'expensify-common/dist/fastMerge'; import Onyx from 'react-native-onyx'; import OnyxCache from 'react-native-onyx/dist/OnyxCache'; +import type {ApiCommand} from '@libs/API/types'; import Log from '@libs/Log'; import PaginationUtils from '@libs/PaginationUtils'; import CONST from '@src/CONST'; @@ -10,9 +11,35 @@ import type {Pages, Request} from '@src/types/onyx'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Middleware from './types'; -function isPaginatedRequest( - request: Request | PaginatedRequest, -): request is PaginatedRequest { +type PaginationConfig = { + initialCommand: ApiCommand; + previousCommand: ApiCommand; + nextCommand: ApiCommand; + resourceCollectionKey: TResourceKey; + pageCollectionKey: TPageKey; + sortItems: (items: OnyxValues[TResourceKey]) => Array ? TResource : never>; + getItemID: (item: OnyxValues[TResourceKey] extends Record ? TResource : never) => string; +}; + +type PaginationConfigMapValue = Omit, 'initialCommand' | 'previousCommand' | 'nextCommand'> & { + type: 'initial' | 'next' | 'previous'; +}; + +const paginationConfigs = new Map(); + +function registerPaginationConfig({ + initialCommand, + previousCommand, + nextCommand, + ...config +}: PaginationConfig): void { + // TODO: Is there a way to avoid these casts? + paginationConfigs.set(initialCommand, {...config, type: 'initial'} as unknown as PaginationConfigMapValue); + paginationConfigs.set(previousCommand, {...config, type: 'previous'} as unknown as PaginationConfigMapValue); + paginationConfigs.set(nextCommand, {...config, type: 'next'} as unknown as PaginationConfigMapValue); +} + +function isPaginatedRequest(request: Request | PaginatedRequest): request is PaginatedRequest { return 'isPaginated' in request && request.isPaginated; } @@ -28,11 +55,13 @@ function isPaginatedRequest { - if (!isPaginatedRequest(request)) { + const paginationConfig = paginationConfigs.get(request.command); + if (!paginationConfig || !isPaginatedRequest(request)) { return requestResponse; } - const {resourceCollectionKey, resourceID, pageCollectionKey, sortItems, getItemID, requestType} = request; + const {resourceCollectionKey, pageCollectionKey, sortItems, getItemID, type} = paginationConfig; + const {resourceID, cursorID} = request; return requestResponse.then((response) => { if (!response?.onyxData) { return Promise.resolve(response); @@ -52,9 +81,9 @@ const Pagination: Middleware = (requestResponse, request) => { const newPage = sortedPageItems.map((item) => getItemID(item)); - // Detect if we are at the start of the list. This will always be the case for the initial request. + // Detect if we are at the start of the list. This will always be the case for the initial request with no cursor. // For previous requests we check that no new data is returned. Ideally the server would return that info. - if (requestType === 'initial' || (requestType === 'next' && newPage.length === 1)) { + if ((type === 'initial' && !cursorID) || (type === 'next' && newPage.length === 1 && newPage[0] === cursorID)) { newPage.unshift(CONST.PAGINATION_START_ID); } @@ -75,4 +104,4 @@ const Pagination: Middleware = (requestResponse, request) => { }); }; -export default Pagination; +export {Pagination, registerPaginationConfig}; diff --git a/src/libs/Middleware/index.ts b/src/libs/Middleware/index.ts index 6fbc3a43fdce..7f02e23ad9b8 100644 --- a/src/libs/Middleware/index.ts +++ b/src/libs/Middleware/index.ts @@ -1,6 +1,6 @@ import HandleUnusedOptimisticID from './HandleUnusedOptimisticID'; import Logging from './Logging'; -import Pagination from './Pagination'; +import {Pagination} from './Pagination'; import Reauthentication from './Reauthentication'; import RecheckConnection from './RecheckConnection'; import SaveResponseInOnyx from './SaveResponseInOnyx'; diff --git a/src/libs/Middleware/types.ts b/src/libs/Middleware/types.ts index 9c9a48ac491d..794143123768 100644 --- a/src/libs/Middleware/types.ts +++ b/src/libs/Middleware/types.ts @@ -1,8 +1,7 @@ -import type {OnyxCollectionKey, OnyxPagesKey} from '@src/ONYXKEYS'; import type Request from '@src/types/onyx/Request'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -type Middleware = (response: Promise, request: Request | PaginatedRequest, isFromSequentialQueue: boolean) => Promise; +type Middleware = (response: Promise, request: Request | PaginatedRequest, isFromSequentialQueue: boolean) => Promise; export default Middleware; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index eb6fd5baa21a..e889455a5e6d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -57,6 +57,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import isPublicScreenRoute from '@libs/isPublicScreenRoute'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; +import {registerPaginationConfig} from '@libs/Middleware/Pagination'; import Navigation from '@libs/Navigation/Navigation'; import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; @@ -96,7 +97,6 @@ import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; -import type {PaginationConfig} from '@src/types/onyx/Request'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; @@ -277,6 +277,16 @@ Onyx.connect({ callback: (val) => (quickAction = val), }); +registerPaginationConfig({ + initialCommand: WRITE_COMMANDS.OPEN_REPORT, + previousCommand: READ_COMMANDS.GET_OLDER_ACTIONS, + nextCommand: READ_COMMANDS.GET_NEWER_ACTIONS, + resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, + sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + getItemID: (reportAction) => reportAction.reportActionID, +}); + function clearGroupChat() { Onyx.set(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, null); } @@ -946,13 +956,9 @@ function openReport( parameters.clientLastReadTime = currentReportData?.[reportID]?.lastReadTime ?? ''; - const paginationConfig: PaginationConfig = { - resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + const paginationConfig = { resourceID: reportID, - pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - getItemID: (reportAction) => reportAction.reportActionID, - requestType: !reportActionID ? 'initial' : 'link', + cursorID: reportActionID, }; if (isFromDeepLink) { @@ -1118,12 +1124,8 @@ function getOlderActions(reportID: string, reportActionID: string) { parameters, {optimisticData, successData, failureData}, { - resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, resourceID: reportID, - pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - getItemID: (reportAction) => reportAction.reportActionID, - requestType: 'previous', + cursorID: reportActionID, }, ); } @@ -1176,12 +1178,8 @@ function getNewerActions(reportID: string, reportActionID: string) { parameters, {optimisticData, successData, failureData}, { - resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, resourceID: reportID, - pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - getItemID: (reportAction) => reportAction.reportActionID, - requestType: 'next', + cursorID: reportActionID, }, ); } diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 780632020e71..41cffdce72f7 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -1,5 +1,4 @@ import type {OnyxUpdate} from 'react-native-onyx'; -import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; import type Response from './Response'; type OnyxData = { @@ -27,16 +26,13 @@ type RequestData = { type Request = RequestData & OnyxData; -type PaginationConfig = { - resourceCollectionKey: TResourceKey; +type PaginationConfig = { resourceID: string; - pageCollectionKey: TPageKey; - sortItems: (items: OnyxValues[TResourceKey]) => Array ? TResource : never>; - getItemID: (item: OnyxValues[TResourceKey] extends Record ? TResource : never) => string; - requestType: 'initial' | 'link' | 'next' | 'previous'; + cursorID?: string | null; }; -type PaginatedRequest = Request & - PaginationConfig & { + +type PaginatedRequest = Request & + PaginationConfig & { isPaginated: true; }; From 1ea212ef4c480260dea571b1aaf881e08413f956 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 7 Jun 2024 12:01:49 +0200 Subject: [PATCH 046/512] Minor UI fixes --- src/components/SettlementButton.tsx | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportUtils.ts | 10 ++++++---- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 62b0c2ee89ac..d06d514d5657 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -227,6 +227,7 @@ function SettlementButton({ text: translate('iou.settleBusiness', {formattedAmount}), icon: Expensicons.Building, value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), subMenuItems: [ { text: translate('iou.payElsewhere', {formattedAmount: ''}), diff --git a/src/languages/en.ts b/src/languages/en.ts index 71148ce876e0..5cfa2a165fd1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -679,6 +679,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', individual: 'Individual', + business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 0e88abd18860..275184d6d5a2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -673,6 +673,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', individual: 'Individual', + business: 'Empresa', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d53f0ba2186d..8681f52d2538 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2074,13 +2074,14 @@ function getIcons( if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([report?.invoiceReceiver.accountID], personalDetails)); } else { - const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); + const receiverPolicyID = report?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { icons.push({ source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), type: CONST.ICON_TYPE_WORKSPACE, name: receiverPolicy.name, - id: receiverPolicy.id, + id: receiverPolicyID, }); } } @@ -2153,14 +2154,15 @@ function getIcons( return icons; } - const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); + const receiverPolicyID = invoiceRoomReport?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { icons.push({ source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), type: CONST.ICON_TYPE_WORKSPACE, name: receiverPolicy.name, - id: receiverPolicy.id, + id: receiverPolicyID, }); } From 2fa4d41f6f2f9e0819e5b0960be355af364c8978 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sat, 8 Jun 2024 01:39:30 -0400 Subject: [PATCH 047/512] UI tests wip --- __mocks__/react-native.ts | 5 +- .../linkingConfig/subscribe/index.native.ts | 2 +- src/pages/home/ReportScreen.tsx | 1 + tests/ui/PaginationTest.tsx | 142 +++++++++++++----- tests/ui/UnreadIndicatorsTest.tsx | 2 +- tests/utils/TestHelper.ts | 26 ++-- 6 files changed, 123 insertions(+), 55 deletions(-) diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 27b78b308446..e884a288aefb 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -98,7 +98,10 @@ jest.doMock('react-native', () => { // so it seems easier to just run the callback immediately in tests. InteractionManager: { ...ReactNative.InteractionManager, - runAfterInteractions: (callback: () => void) => callback(), + runAfterInteractions: (callback: () => void) => { + callback(); + return {cancel: () => {}}; + }, }, }, ReactNative, diff --git a/src/libs/Navigation/linkingConfig/subscribe/index.native.ts b/src/libs/Navigation/linkingConfig/subscribe/index.native.ts index 061bca092b7d..46720e9884e9 100644 --- a/src/libs/Navigation/linkingConfig/subscribe/index.native.ts +++ b/src/libs/Navigation/linkingConfig/subscribe/index.native.ts @@ -12,7 +12,7 @@ import SCREENS from '@src/SCREENS'; // This field in linkingConfig is supported on native only. const subscribe: LinkingOptions['subscribe'] = (listener) => { - // We need to ovverride the default behaviour for the deep link to search screen. + // We need to override the default behaviour for the deep link to search screen. // Even on mobile narrow layout, this screen need to push two screens on the stack to work (bottom tab and central pane). // That's why we are going to handle it with our navigate function instead the default react-navigation one. const linkingSubscription = Linking.addEventListener('url', ({url}) => { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 496448555103..f86a49385956 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -708,6 +708,7 @@ function ReportScreen({ {shouldShowReportActionList && ( { transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); }; - const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback) => { + const addListener: jest.Mock = jest.fn().mockImplementation((listener: string, callback: () => void) => { if (listener === 'transitionEnd') { transitionEndListeners.push(callback); } @@ -102,6 +93,7 @@ jest.mock('@react-navigation/native', () => { routes: [], }), addListener, + setParams: jest.fn(), } as typeof NativeNavigation.useNavigation); return { @@ -116,7 +108,7 @@ jest.mock('@react-navigation/native', () => { const fetchMock = TestHelper.getGlobalFetchMock(); beforeAll(() => { - global.fetch = fetchMock; + global.fetch = fetchMock as unknown as typeof global.fetch; Linking.setInitialURL('https://new.expensify.com/'); appSetup(); @@ -151,19 +143,56 @@ function scrollToOffset(offset: number) { }); } -function navigateToSidebar(): Promise { - const hintText = Localize.translateLocal('accessibilityHints.navigateToChatsList'); - const reportHeaderBackButton = screen.queryByAccessibilityHint(hintText); - if (reportHeaderBackButton) { - fireEvent(reportHeaderBackButton, 'press'); - } - return waitForBatchedUpdates(); +function getReportActions() { + const messageHintText = Localize.translateLocal('accessibilityHints.chatMessage'); + return screen.queryAllByLabelText(messageHintText); +} + +function triggerListLayout() { + fireEvent(screen.getByTestId('report-actions-view-container'), 'onLayout', { + nativeEvent: { + layout: { + x: 0, + y: 0, + width: 300, + height: 300, + }, + }, + }); + fireEvent(screen.getByTestId('report-actions-list'), 'onLayout', { + nativeEvent: { + layout: { + x: 0, + y: 0, + width: 300, + height: 300, + }, + }, + }); + + getReportActions().forEach((e, i) => + fireEvent(e, 'onLayout', { + nativeEvent: { + layout: { + x: 0, + y: i * 100, + width: 300, + height: 100, + }, + }, + }), + ); } async function navigateToSidebarOption(index: number): Promise { const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); const optionRows = screen.queryAllByAccessibilityHint(hintText); fireEvent(optionRows[index], 'press'); + await act(() => { + transitionEndCB?.(); + }); + // ReportScreen relies on the onLayout event to receive updates from onyx. + triggerListLayout(); await waitForBatchedUpdatesWithAct(); } @@ -196,42 +225,73 @@ function signInAndGetApp(): Promise { return waitForBatchedUpdates(); }) .then(async () => { - // Simulate setting an unread report and personal details - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - lastMessageText: 'Test', - participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, - lastActorAccountID: USER_B_ACCOUNT_ID, - type: CONST.REPORT.TYPE.CHAT, - }); + await act(async () => { + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastMessageText: 'Test', + participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }); - await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); - // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(); + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + }); - return waitForBatchedUpdatesWithAct(); + await waitForBatchedUpdatesWithAct(); }); } describe('Pagination', () => { afterEach(async () => { - await Onyx.clear(); + await act(async () => { + await Onyx.clear(); - // Unsubscribe to pusher channels - PusherHelper.teardown(); + // Unsubscribe to pusher channels + PusherHelper.teardown(); + }); await waitForBatchedUpdatesWithAct(); jest.clearAllMocks(); }); - it('opens a chat and load initial messages', async () => { + it.only('opens a chat and load initial messages', async () => { + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + fetchMock.mockAPICommand('OpenReport', [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + value: { + // '1': { + // reportActionID: '1', + // actionName: 'CREATED', + // created: format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING), + // }, + '2': TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2'), + '3': TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '3'), + }, + }, + ]); await signInAndGetApp(); await navigateToSidebarOption(0); - await act(() => transitionEndCB?.()); + + const messageHintText = Localize.translateLocal('accessibilityHints.chatMessage'); + const messages = screen.queryAllByLabelText(messageHintText); + + expect(fetchMock.mock.calls.filter((c) => c[0] === 'https://www.expensify.com.dev/api/OpenReport?')).toHaveLength(1); + expect(messages).toHaveLength(2); + + // Scrolling up here should not trigger a new network request. + const fetchCalls = fetchMock.mock.calls.length; + scrollToOffset(300); + await waitForBatchedUpdatesWithAct(); + expect(fetchMock.mock.calls.length).toBe(fetchCalls); }); }); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index a269c8476c02..e963e0f155b8 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -120,7 +120,7 @@ beforeAll(() => { // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as any; Linking.setInitialURL('https://new.expensify.com/'); appSetup(); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index e6fed165382d..bbf2ec373353 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -8,12 +8,12 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; -type MockFetch = ReturnType & { - pause?: () => void; - fail?: () => void; - succeed?: () => void; - resume?: () => Promise; - mockAPICommand?: (command: string, response: OnyxResponse['onyxData']) => void; +type MockFetch = jest.Mock & { + pause: () => void; + fail: () => void; + succeed: () => void; + resume: () => Promise; + mockAPICommand: (command: string, response: OnyxResponse['onyxData']) => void; }; type QueueItem = { @@ -175,22 +175,26 @@ function getGlobalFetchMock() { } : { ok: true, - json: () => { + json: async () => { const commandMatch = typeof input === 'string' ? input.match(/https:\/\/www.expensify.com.dev\/api\/(\w+)\?/) : null; const command = commandMatch ? commandMatch[1] : null; + if (command && responses.has(command)) { + return Promise.resolve({jsonCode: 200, onyxData: responses.get(command)}); + } + return Promise.resolve({jsonCode: 200}); }, }; - const mockFetch: MockFetch = jest.fn().mockImplementation((input: RequestInfo) => { + const mockFetch = jest.fn().mockImplementation((input: RequestInfo) => { if (!isPaused) { return Promise.resolve(getResponse(input)); } return new Promise((resolve) => { queue.push({resolve, input}); }); - }); + }) as MockFetch; const baseMockReset = mockFetch.mockReset.bind(mockFetch); mockFetch.mockReset = () => { @@ -205,7 +209,7 @@ function getGlobalFetchMock() { mockFetch.pause = () => (isPaused = true); mockFetch.resume = () => { isPaused = false; - queue.forEach(({resolve, input, init}) => resolve(getResponse(input, init))); + queue.forEach(({resolve, input}) => resolve(getResponse(input))); return waitForBatchedUpdates(); }; mockFetch.fail = () => (shouldFail = true); @@ -213,7 +217,7 @@ function getGlobalFetchMock() { mockFetch.mockAPICommand = (command: string, response: OnyxResponse['onyxData']) => { responses.set(command, response); }; - return mockFetch as typeof fetch; + return mockFetch; } function setPersonalDetails(login: string, accountID: number) { From 97bf8d647d2caa81cce8d2bd51f45a0edb4a3fec Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 10 Jun 2024 10:07:04 +0200 Subject: [PATCH 048/512] Improve the check --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8681f52d2538..3ae370112a0c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1872,7 +1872,7 @@ function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry< let secondaryAvatar: Icon; if (displayAllActors) { - if (!isIndividualInvoiceRoom(chatReport)) { + if (isInvoiceRoom(chatReport) && !isIndividualInvoiceRoom(chatReport)) { const secondaryPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : '-1'; const secondaryPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${secondaryPolicyID}`]; const secondaryPolicyAvatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); From 3b9cc3bc5dd5381ca859c0220282ba382cd70da3 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 10 Jun 2024 11:54:29 +0200 Subject: [PATCH 049/512] Bring back refactored AddressPage --- src/components/AddressForm.tsx | 1 + src/components/CountrySelector.tsx | 28 +++- src/components/StateSelector.tsx | 4 +- ...useGeographicalStateAndCountryFromRoute.ts | 27 ++++ src/hooks/useGeographicalStateFromRoute.ts | 23 --- .../ModalStackNavigators/index.tsx | 4 +- src/pages/AddressPage.tsx | 107 ++++++++++++ .../Profile/PersonalDetails/AddressPage.tsx | 153 ------------------ .../PersonalDetails/PersonalAddressPage.tsx | 61 +++++++ .../workspace/WorkspaceProfileAddressPage.tsx | 117 +++----------- 10 files changed, 248 insertions(+), 277 deletions(-) create mode 100644 src/hooks/useGeographicalStateAndCountryFromRoute.ts delete mode 100644 src/hooks/useGeographicalStateFromRoute.ts create mode 100644 src/pages/AddressPage.tsx delete mode 100644 src/pages/settings/Profile/PersonalDetails/AddressPage.tsx create mode 100644 src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 9ad4643e834a..89456ae944fa 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -183,6 +183,7 @@ function AddressForm({ InputComponent={CountrySelector} inputID={INPUT_IDS.COUNTRY} value={country} + onValueChange={onAddressChanged} shouldSaveDraft={shouldSaveDraft} /> diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 002c0c6d4b0a..b8558c6bd92b 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {forwardRef, useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; +import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -32,6 +33,7 @@ type CountrySelectorProps = { function CountrySelector({errorText = '', value: countryCode, onInputChange = () => {}, onBlur}: CountrySelectorProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const {country: countryFromUrl} = useGeographicalStateAndCountryFromRoute(); const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; @@ -39,12 +41,30 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = () const didOpenContrySelector = useRef(false); const isFocused = useIsFocused(); useEffect(() => { - if (!isFocused || !didOpenContrySelector.current) { + // Check if the country selector was opened and no value was selected, triggering onBlur to display an error + if (isFocused && didOpenContrySelector.current) { + didOpenContrySelector.current = false; + if (!countryFromUrl) { + onBlur?.(); + } + } + + // If no country is selected from the URL, exit the effect early to avoid further processing. + if (!countryFromUrl) { return; } - didOpenContrySelector.current = false; - onBlur?.(); - }, [isFocused, onBlur]); + + // If a country is selected, invoke `onInputChange` to update the form and clear any validation errors related to the country selection. + if (onInputChange) { + onInputChange(countryFromUrl); + } + + // Clears the `country` parameter from the URL to ensure the component country is driven by the parent component rather than URL parameters. + // This helps prevent issues where the component might not update correctly if the country is controlled by both the parent and the URL. + Navigation.setParams({country: undefined}); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [countryFromUrl, isFocused, onBlur]); useEffect(() => { // This will cause the form to revalidate and remove any error related to country name diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx index 67ba80c13ef8..8a1d85106cf3 100644 --- a/src/components/StateSelector.tsx +++ b/src/components/StateSelector.tsx @@ -3,7 +3,7 @@ import {CONST as COMMON_CONST} from 'expensify-common'; import React, {useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; +import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -44,7 +44,7 @@ function StateSelector( ) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const stateFromUrl = useGeographicalStateFromRoute(); + const {state: stateFromUrl} = useGeographicalStateAndCountryFromRoute(); const didOpenStateSelector = useRef(false); const isFocused = useIsFocused(); diff --git a/src/hooks/useGeographicalStateAndCountryFromRoute.ts b/src/hooks/useGeographicalStateAndCountryFromRoute.ts new file mode 100644 index 000000000000..b94644bdd287 --- /dev/null +++ b/src/hooks/useGeographicalStateAndCountryFromRoute.ts @@ -0,0 +1,27 @@ +import {useRoute} from '@react-navigation/native'; +import {CONST as COMMON_CONST} from 'expensify-common'; +import CONST from '@src/CONST'; + +type State = keyof typeof COMMON_CONST.STATES; +type Country = keyof typeof CONST.ALL_COUNTRIES; +type StateAndCountry = {state?: State; country?: Country}; + +/** + * Extracts the 'state' and 'country' query parameters from the route/ url and validates it against COMMON_CONST.STATES and CONST.ALL_COUNTRIES. + * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: state=MO + * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: state=undefined + * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: state=undefined + * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: state=MO + * Similarly for country parameter. + */ +export default function useGeographicalStateAndCountryFromRoute(stateParamName = 'state', countryParamName = 'country'): StateAndCountry { + const routeParams = useRoute().params as Record; + + const stateFromUrlTemp = routeParams?.[stateParamName] as string | undefined; + const countryFromUrlTemp = routeParams?.[countryParamName] as string | undefined; + + return { + state: COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO, + country: Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFromUrlTemp) as Country, + }; +} diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts deleted file mode 100644 index 434d4c534d61..000000000000 --- a/src/hooks/useGeographicalStateFromRoute.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import type {ParamListBase, RouteProp} from '@react-navigation/native'; -import {CONST as COMMON_CONST} from 'expensify-common'; - -type CustomParamList = ParamListBase & Record>; -type State = keyof typeof COMMON_CONST.STATES; - -/** - * Extracts the 'state' (default) query parameter from the route/ url and validates it against COMMON_CONST.STATES, returning its ISO code or `undefined`. - * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: MO - * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: undefined - * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: undefined - * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: MO - */ -export default function useGeographicalStateFromRoute(stateParamName = 'state'): State | undefined { - const route = useRoute>(); - const stateFromUrlTemp = route.params?.[stateParamName] as string | undefined; - - if (!stateFromUrlTemp) { - return; - } - return COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO; -} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 807c938e21dd..6ab8398c11d7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -181,7 +181,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/TimezoneSelectPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: () => require('../../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: () => require('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default as React.ComponentType, - [SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType, + [SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: () => require('../../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default as React.ComponentType, @@ -195,7 +195,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/AppDownloadLinks').default as React.ComponentType, [SCREENS.SETTINGS.CONSOLE]: () => require('../../../../pages/settings/AboutPage/ConsolePage').default as React.ComponentType, [SCREENS.SETTINGS.SHARE_LOG]: () => require('../../../../pages/settings/AboutPage/ShareLogPage').default as React.ComponentType, - [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: () => require('../../../../pages/settings/Wallet/ActivatePhysicalCardPage').default as React.ComponentType, diff --git a/src/pages/AddressPage.tsx b/src/pages/AddressPage.tsx new file mode 100644 index 000000000000..90711ebbab92 --- /dev/null +++ b/src/pages/AddressPage.tsx @@ -0,0 +1,107 @@ +import React, {useCallback, useEffect, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import AddressForm from '@components/AddressForm'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {FormOnyxValues} from '@src/components/Form/types'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; + +type AddressPageProps = { + /** User's private personal details */ + address?: Address; + /** Whether app is loading */ + isLoadingApp: OnyxEntry; + /** Function to call when address form is submitted */ + updateAddress: (values: FormOnyxValues) => void; + /** Title of address page */ + title: string; +}; + +function AddressPage({title, address, updateAddress, isLoadingApp = true}: AddressPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + // Check if country is valid + const {street, street2} = address ?? {}; + const [currentCountry, setCurrentCountry] = useState(address?.country); + const [state, setState] = useState(address?.state); + const [city, setCity] = useState(address?.city); + const [zipcode, setZipcode] = useState(address?.zip); + + useEffect(() => { + if (!address) { + return; + } + setState(address.state); + setCurrentCountry(address.country); + setCity(address.city); + setZipcode(address.zip); + }, [address]); + + const handleAddressChange = useCallback((value: unknown, key: unknown) => { + const addressPart = value as string; + const addressPartKey = key as keyof Address; + + if (addressPartKey !== 'country' && addressPartKey !== 'state' && addressPartKey !== 'city' && addressPartKey !== 'zipPostCode') { + return; + } + if (addressPartKey === 'country') { + setCurrentCountry(addressPart as Country | ''); + setState(''); + setCity(''); + setZipcode(''); + return; + } + if (addressPartKey === 'state') { + setState(addressPart); + setCity(''); + setZipcode(''); + return; + } + if (addressPartKey === 'city') { + setCity(addressPart); + setZipcode(''); + return; + } + setZipcode(addressPart); + }, []); + + return ( + + Navigation.goBack()} + /> + {isLoadingApp ? ( + + ) : ( + + )} + + ); +} + +AddressPage.displayName = 'AddressPage'; + +export default AddressPage; diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx b/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx deleted file mode 100644 index fcb018348b72..000000000000 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import AddressForm from '@components/AddressForm'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import type {FormOnyxValues} from '@src/components/Form/types'; -import CONST from '@src/CONST'; -import type {Country} from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {PrivatePersonalDetails} from '@src/types/onyx'; -import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; - -type AddressPageOnyxProps = { - /** User's private personal details */ - privatePersonalDetails: OnyxEntry; - /** Whether app is loading */ - isLoadingApp: OnyxEntry; -}; - -type AddressPageProps = StackScreenProps & AddressPageOnyxProps; - -/** - * Submit form to update user's first and last legal name - * @param values - form input values - */ -function updateAddress(values: FormOnyxValues) { - PersonalDetails.updateAddress( - values.addressLine1?.trim() ?? '', - values.addressLine2?.trim() ?? '', - values.city.trim(), - values.state.trim(), - values?.zipPostCode?.trim().toUpperCase() ?? '', - values.country, - ); -} - -function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: AddressPageProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const address = useMemo(() => privatePersonalDetails?.address, [privatePersonalDetails]); - const countryFromUrlTemp = route?.params?.country; - - // Check if country is valid - const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; - const stateFromUrl = useGeographicalStateFromRoute(); - const [currentCountry, setCurrentCountry] = useState(address?.country); - const [street1, street2] = (address?.street ?? '').split('\n'); - const [state, setState] = useState(address?.state); - const [city, setCity] = useState(address?.city); - const [zipcode, setZipcode] = useState(address?.zip); - - useEffect(() => { - if (!address) { - return; - } - setState(address.state); - setCurrentCountry(address.country); - setCity(address.city); - setZipcode(address.zip); - }, [address]); - - const handleAddressChange = useCallback((value: unknown, key: unknown) => { - const countryValue = value as Country | ''; - const addressKey = key as keyof Address; - - if (addressKey !== 'country' && addressKey !== 'state' && addressKey !== 'city' && addressKey !== 'zipPostCode') { - return; - } - if (addressKey === 'country') { - setCurrentCountry(countryValue); - setState(''); - setCity(''); - setZipcode(''); - return; - } - if (addressKey === 'state') { - setState(countryValue); - setCity(''); - setZipcode(''); - return; - } - if (addressKey === 'city') { - setCity(countryValue); - setZipcode(''); - return; - } - setZipcode(countryValue); - }, []); - - useEffect(() => { - if (!countryFromUrl) { - return; - } - handleAddressChange(countryFromUrl, 'country'); - }, [countryFromUrl, handleAddressChange]); - - useEffect(() => { - if (!stateFromUrl) { - return; - } - handleAddressChange(stateFromUrl, 'state'); - }, [handleAddressChange, stateFromUrl]); - - return ( - - Navigation.goBack()} - /> - {isLoadingApp ? ( - - ) : ( - - )} - - ); -} - -AddressPage.displayName = 'AddressPage'; - -export default withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, -})(AddressPage); diff --git a/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx b/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx new file mode 100644 index 000000000000..85402137fe6d --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx @@ -0,0 +1,61 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import AddressPage from '@pages/AddressPage'; +import * as PersonalDetails from '@userActions/PersonalDetails'; +import type {FormOnyxValues} from '@src/components/Form/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; + +type PersonalAddressPageOnyxProps = { + /** User's private personal details */ + privatePersonalDetails: OnyxEntry; + /** Whether app is loading */ + isLoadingApp: OnyxEntry; +}; + +type PersonalAddressPageProps = StackScreenProps & PersonalAddressPageOnyxProps; + +/** + * Submit form to update user's first and last legal name + * @param values - form input values + */ +function updateAddress(values: FormOnyxValues) { + PersonalDetails.updateAddress( + values.addressLine1?.trim() ?? '', + values.addressLine2?.trim() ?? '', + values.city.trim(), + values.state.trim(), + values?.zipPostCode?.trim().toUpperCase() ?? '', + values.country, + ); +} + +function PersonalAddressPage({privatePersonalDetails, isLoadingApp = true}: PersonalAddressPageProps) { + const {translate} = useLocalize(); + const address = useMemo(() => privatePersonalDetails?.address, [privatePersonalDetails]); + + return ( + + ); +} + +PersonalAddressPage.displayName = 'PersonalAddressPage'; + +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(PersonalAddressPage); diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceProfileAddressPage.tsx index c7cf00efb798..47793f7fb810 100644 --- a/src/pages/workspace/WorkspaceProfileAddressPage.tsx +++ b/src/pages/workspace/WorkspaceProfileAddressPage.tsx @@ -1,21 +1,14 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import AddressForm from '@components/AddressForm'; +import React, {useMemo} from 'react'; import type {FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import AddressPage from '@pages/AddressPage'; import {updateAddress} from '@userActions/Policy/Policy'; -import type {Country} from '@src/CONST'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import type ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {CompanyAddress} from '@src/types/onyx/Policy'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import type {WithPolicyProps} from './withPolicy'; import withPolicy from './withPolicy'; @@ -23,18 +16,21 @@ type WorkspaceProfileAddressPagePolicyProps = WithPolicyProps; type WorkspaceProfileAddressPageProps = StackScreenProps & WorkspaceProfileAddressPagePolicyProps; -function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPageProps) { - const styles = useThemeStyles(); +function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) { const {translate} = useLocalize(); - const address = useMemo(() => policy?.address, [policy]); - const [currentCountry, setCurrentCountry] = useState(address?.country); - const [[street1, street2], setStreets] = useState((address?.addressStreet ?? '').split('\n')); - const [state, setState] = useState(address?.state); - const [city, setCity] = useState(address?.city); - const [zipcode, setZipcode] = useState(address?.zipCode); - - const countryFromUrlTemp = route?.params?.country; - const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; + const address: Address = useMemo(() => { + const tempAddress = policy?.address; + const [street1, street2] = (tempAddress?.addressStreet ?? '').split('\n'); + const result = { + street: street1?.trim() ?? '', + street2: street2?.trim() ?? '', + city: tempAddress?.city?.trim() ?? '', + state: tempAddress?.state?.trim() ?? '', + zip: tempAddress?.zipCode?.trim().toUpperCase() ?? '', + country: tempAddress?.country ?? '', + }; + return result; + }, [policy]); const updatePolicyAddress = (values: FormOnyxValues) => { if (!policy) { @@ -50,78 +46,13 @@ function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPag Navigation.goBack(); }; - const handleAddressChange = useCallback((value: unknown, key: unknown) => { - const countryValue = value as Country | ''; - const addressKey = key as keyof CompanyAddress; - - if (addressKey !== 'country' && addressKey !== 'state' && addressKey !== 'city' && addressKey !== 'zipCode') { - return; - } - if (addressKey === 'country') { - setCurrentCountry(countryValue); - setState(''); - setCity(''); - setZipcode(''); - return; - } - if (addressKey === 'state') { - setState(countryValue); - setCity(''); - setZipcode(''); - return; - } - if (addressKey === 'city') { - setCity(countryValue); - setZipcode(''); - return; - } - setZipcode(countryValue); - }, []); - - useEffect(() => { - if (!address) { - return; - } - setStreets((address?.addressStreet ?? '').split('\n')); - setState(address.state); - setCurrentCountry(address.country); - setCity(address.city); - setZipcode(address.zipCode); - }, [address]); - - useEffect(() => { - if (!countryFromUrl) { - return; - } - handleAddressChange(countryFromUrl, 'country'); - }, [countryFromUrl, handleAddressChange]); - return ( - - Navigation.goBack()} - /> - - {translate('workspace.editor.addressContext')} - - - + ); } From 84c1c6c8e523b0247366c0706c33605c01333db5 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 10 Jun 2024 10:30:02 -0700 Subject: [PATCH 050/512] Bump type-fest --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c01cc9d61c27..99137b3c3261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -239,7 +239,7 @@ "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "type-fest": "4.18.3", + "type-fest": "4.20.0", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", @@ -35990,9 +35990,9 @@ } }, "node_modules/type-fest": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.3.tgz", - "integrity": "sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", + "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==", "dev": true, "engines": { "node": ">=16" diff --git a/package.json b/package.json index 1763e161e7bf..d701ad9e71d3 100644 --- a/package.json +++ b/package.json @@ -291,7 +291,7 @@ "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "type-fest": "4.18.3", + "type-fest": "4.20.0", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", From 4161096b4de10c56e271ca8a705519b6d8be32a5 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 10 Jun 2024 22:15:51 -0400 Subject: [PATCH 051/512] Use Onyx.connect instead of OnyxCache --- src/libs/Middleware/Pagination.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 314db6643d89..9a9f063c99e8 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -1,13 +1,12 @@ // TODO: Is this a legit use case for exposing `OnyxCache`, or should we use `Onyx.connect`? import fastMerge from 'expensify-common/dist/fastMerge'; import Onyx from 'react-native-onyx'; -import OnyxCache from 'react-native-onyx/dist/OnyxCache'; import type {ApiCommand} from '@libs/API/types'; import Log from '@libs/Log'; import PaginationUtils from '@libs/PaginationUtils'; import CONST from '@src/CONST'; import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS'; -import type {Pages, Request} from '@src/types/onyx'; +import type {Request} from '@src/types/onyx'; import type {PaginatedRequest} from '@src/types/onyx/Request'; import type Middleware from './types'; @@ -26,6 +25,8 @@ type PaginationConfigMapValue = Omit(); +const ressources = new Map(); +const pages = new Map(); function registerPaginationConfig({ initialCommand, @@ -37,6 +38,18 @@ function registerPaginationConfig { + ressources.set(config.resourceCollectionKey, data); + }, + }); + Onyx.connect({ + key: config.pageCollectionKey, + callback: (data) => { + pages.set(config.pageCollectionKey, data); + }, + }); } function isPaginatedRequest(request: Request | PaginatedRequest): request is PaginatedRequest { @@ -87,11 +100,11 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - const existingItems = (OnyxCache.getValue(resourceKey) ?? {}) as OnyxValues[typeof resourceCollectionKey]; + const existingItems = ressources.get(resourceCollectionKey) ?? {}; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); - const existingPages = (OnyxCache.getValue(pageKey) ?? []) as Pages; + const existingPages = pages.get(pageCollectionKey) ?? []; const mergedPages = PaginationUtils.mergeContinuousPages(sortedAllItems, [...existingPages, newPage], getItemID); response.onyxData.push({ From 70aef20cdc81528e501ebae2ad7e32c60ff806ee Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 10 Jun 2024 23:13:19 -0400 Subject: [PATCH 052/512] Fixes --- src/libs/Middleware/Pagination.ts | 19 +++++++++++++------ src/libs/PaginationUtils.ts | 10 +++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 9a9f063c99e8..ad1bbad4ba2a 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -1,5 +1,6 @@ // TODO: Is this a legit use case for exposing `OnyxCache`, or should we use `Onyx.connect`? import fastMerge from 'expensify-common/dist/fastMerge'; +import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ApiCommand} from '@libs/API/types'; import Log from '@libs/Log'; @@ -25,8 +26,8 @@ type PaginationConfigMapValue = Omit(); -const ressources = new Map(); -const pages = new Map(); +const ressources = new Map>(); +const pages = new Map>(); function registerPaginationConfig({ initialCommand, @@ -39,13 +40,17 @@ function registerPaginationConfig { ressources.set(config.resourceCollectionKey, data); }, }); Onyx.connect({ - key: config.pageCollectionKey, + // TODO: Also not sure why this cast is needed. + key: config.pageCollectionKey as OnyxPagesKey, + waitForCollectionCallback: true, callback: (data) => { pages.set(config.pageCollectionKey, data); }, @@ -100,11 +105,13 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - const existingItems = ressources.get(resourceCollectionKey) ?? {}; + const resourceCollections = ressources.get(resourceCollectionKey) ?? {}; + const existingItems = resourceCollections[resourceKey] ?? {}; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); - const existingPages = pages.get(pageCollectionKey) ?? []; + const pagesCollections = pages.get(pageCollectionKey) ?? {}; + const existingPages = pagesCollections[pageKey] ?? []; const mergedPages = PaginationUtils.mergeContinuousPages(sortedAllItems, [...existingPages, newPage], getItemID); response.onyxData.push({ diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 89edafafc2f9..a1ef26bd9e9c 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -86,15 +86,15 @@ function mergeContinuousPages(sortedItems: TResource[], pages: Pages, // Pages need to be sorted by firstIndex ascending then by lastIndex descending const sortedPages = pagesWithIndexes.sort((a, b) => { - if (a.firstID === CONST.PAGINATION_START_ID) { - return -1; + if (a.firstIndex !== b.firstIndex) { + if (a.firstID === CONST.PAGINATION_START_ID) { + return -1; + } + return a.firstIndex - b.firstIndex; } if (a.lastID === CONST.PAGINATION_END_ID) { return 1; } - if (a.firstIndex !== b.firstIndex) { - return a.firstIndex - b.firstIndex; - } return b.lastIndex - a.lastIndex; }); From 7bd1d725feb70cb5cfee306828df1f968523f644 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 11 Jun 2024 13:04:09 +0800 Subject: [PATCH 053/512] add new copy for paying a report with all hold expenses --- src/components/ProcessMoneyReportHoldMenu.tsx | 2 +- src/languages/en.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 12e2d818b715..7e2b60a868bc 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -74,7 +74,7 @@ function ProcessMoneyReportHoldMenu({ if (nonHeldAmount) { promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; } else { - promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAmount'; + promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount'; } return translate(promptTranslation); diff --git a/src/languages/en.ts b/src/languages/en.ts index a90d0a5bb4b9..0569c3b1c551 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -769,6 +769,7 @@ export default { confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", + confirmPayAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and pay?', payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', From 7890b03a6ba2630c1e95f27bd09cde8ff353f5cc Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 12 Jun 2024 10:38:10 +0200 Subject: [PATCH 054/512] export onyx state button added --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 1d1b5852f384..c776f7ba0ebd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -951,6 +951,7 @@ export default { deviceCredentials: 'Device credentials', invalidate: 'Invalidate', destroy: 'Destroy', + exportOnyxState: 'Export Onyx state', }, debugConsole: { saveLog: 'Save log', diff --git a/src/languages/es.ts b/src/languages/es.ts index ad2748a25fa2..814431129ed7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -947,6 +947,7 @@ export default { deviceCredentials: 'Credenciales del dispositivo', invalidate: 'Invalidar', destroy: 'Destruir', + exportOnyxState: 'Exportar estado Onyx', }, debugConsole: { saveLog: 'Guardar registro', diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 0424682c7afb..34d75d9279ad 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -53,6 +53,10 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); + const exportOnyxState = () => { + console.log('export state here'); + }; + const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { translationKey: 'initialSettingsPage.troubleshoot.viewConsole', @@ -66,6 +70,11 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { icon: Expensicons.RotateLeft, action: () => setIsConfirmationModalVisible(true), }, + { + translationKey: 'initialSettingsPage.troubleshoot.exportOnyxState', + icon: Expensicons.Download, + action: exportOnyxState + } ]; if (shouldStoreLogs) { From 159f0a1699d0f49656d7fd6d8a65199cdc8ff8e6 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 07:36:44 -0600 Subject: [PATCH 055/512] Simplify OnyxPagesKey type --- src/ONYXKEYS.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 99a144d39790..08ded91e340c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,4 @@ -import type {ConditionalKeys, ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; @@ -711,7 +711,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping; type OnyxValueKey = keyof OnyxValuesMapping; type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey; -type OnyxPagesKey = ConditionalKeys; +type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES; type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ From 61f249cdb0945c87130358a2996bdd53c38a8424 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 07:43:40 -0600 Subject: [PATCH 056/512] Fix API.paginate type overloads --- src/libs/API/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 575477d4220b..67c0f85aa07b 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -189,13 +189,20 @@ function read(command: TCommand, apiCommandParamet }); } -function paginate>( +function paginate>( + type: TRequestType, + command: TCommand, + apiCommandParameters: ApiRequestCommandParameters[TCommand], + onyxData: OnyxData, + config: PaginationConfig, +): Promise; +function paginate>( type: TRequestType, command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData, config: PaginationConfig, -): TRequestType extends typeof CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS ? Promise : void; +): void; function paginate>( type: TRequestType, command: TCommand, From 43caf1e0d2bbd265816d5666882ba1a7492f1521 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 07:51:03 -0600 Subject: [PATCH 057/512] Use a switch statement in API.paginate --- src/libs/API/index.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 67c0f85aa07b..ba6a54ca14f6 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -219,16 +219,18 @@ function paginate processRequest(request, type)); + return; + default: + throw new Error('Unknown API request type'); } - - validateReadyToRead(command as ReadCommand).then(() => processRequest(request, type)); } export {write, makeRequestWithSideEffects, read, paginate}; From ff1883faf646f54730e6f36777e3718b0132c828 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 07:54:01 -0600 Subject: [PATCH 058/512] Remove outdated TODO --- src/libs/Middleware/Pagination.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index ad1bbad4ba2a..acf092739782 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -1,4 +1,3 @@ -// TODO: Is this a legit use case for exposing `OnyxCache`, or should we use `Onyx.connect`? import fastMerge from 'expensify-common/dist/fastMerge'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; From 9b33c677bdb1674b891a00c6fff670f480b43e81 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 07:54:35 -0600 Subject: [PATCH 059/512] Fix typo --- src/libs/Middleware/Pagination.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index acf092739782..544c1f03ee04 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -25,7 +25,7 @@ type PaginationConfigMapValue = Omit(); -const ressources = new Map>(); +const resources = new Map>(); const pages = new Map>(); function registerPaginationConfig({ @@ -43,7 +43,7 @@ function registerPaginationConfig { - ressources.set(config.resourceCollectionKey, data); + resources.set(config.resourceCollectionKey, data); }, }); Onyx.connect({ @@ -104,7 +104,7 @@ const Pagination: Middleware = (requestResponse, request) => { newPage.unshift(CONST.PAGINATION_START_ID); } - const resourceCollections = ressources.get(resourceCollectionKey) ?? {}; + const resourceCollections = resources.get(resourceCollectionKey) ?? {}; const existingItems = resourceCollections[resourceKey] ?? {}; const allItems = fastMerge(existingItems, pageItems, true); const sortedAllItems = sortItems(allItems); From c3200a23d725e67bb874cad589ef40a1c31d95ea Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 08:09:29 -0600 Subject: [PATCH 060/512] Add comments to explain local objects in Pagination actions file --- src/libs/Middleware/Pagination.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 544c1f03ee04..0f82823915aa 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -24,8 +24,13 @@ type PaginationConfigMapValue = Omit(); + +// Local cache of paginated Onyx resources const resources = new Map>(); + +// Local cache of Onyx pages objects const pages = new Map>(); function registerPaginationConfig({ From db30f6bd8ecf9a45c2833bcebd882f0f8fb481e0 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 08:26:37 -0600 Subject: [PATCH 061/512] Extract ItemWithIndex type in PaginationUtils --- src/libs/PaginationUtils.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index a1ef26bd9e9c..4f9be6c398f6 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -2,17 +2,33 @@ import CONST from '@src/CONST'; import type Pages from '@src/types/onyx/Pages'; type PageWithIndex = { + /** The IDs we store in Onyx and which make up the page. */ ids: string[]; + + /** The first ID in the page. */ firstID: string; + + /** The index of the first ID in the page in the complete set of sorted items. */ firstIndex: number; + + /** The last ID in the page. */ lastID: string; + + /** The index of the last ID in the page in the complete set of sorted items. */ lastIndex: number; }; +// It's useful to be able to reference and item along with its index in a sorted array, +// since the index is needed for ordering but the id is what we actually store. +type ItemWithIndex = { + id: string; + index: number; +}; + /** * Finds the id and index in sortedItems of the first item in the given page that's present in sortedItems. */ -function findFirstItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): {id: string; index: number} | null { +function findFirstItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): ItemWithIndex | null { for (const id of page) { if (id === CONST.PAGINATION_START_ID) { return {id, index: 0}; @@ -28,7 +44,7 @@ function findFirstItem(sortedItems: TResource[], page: string[], getI /** * Finds the id and index in sortedItems of the last item in the given page that's present in sortedItems. */ -function findLastItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): {id: string; index: number} | null { +function findLastItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): ItemWithIndex | null { for (const id of page.slice().reverse()) { if (id === CONST.PAGINATION_END_ID) { return {id, index: sortedItems.length - 1}; @@ -78,6 +94,9 @@ function getPagesWithIndexes(sortedItems: TResource[], pages: Pages, .filter((page): page is PageWithIndex => page !== null); } +/** + * Given a sorted array of items and an array of Pages of item IDs, find any overlapping pages and merge them together. + */ function mergeContinuousPages(sortedItems: TResource[], pages: Pages, getItemID: (item: TResource) => string): Pages { const pagesWithIndexes = getPagesWithIndexes(sortedItems, pages, getItemID); if (pagesWithIndexes.length === 0) { From e8d5b41fad150edf7c4d8537db1eedf9462d3096 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 08:55:59 -0600 Subject: [PATCH 062/512] Add comments to PaginationConfig type --- src/types/onyx/Request.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index f9bd37869d91..6c0bbccd4e6f 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -58,13 +58,29 @@ type RequestData = { /** Model of requests sent to the API */ type Request = RequestData & OnyxData; +/** + * An object used to describe how a request can be paginated. + */ type PaginationConfig = { + /** + * The ID of the resource we're trying to paginate (i.e: the reportID in the case of paginating reportActions). + */ resourceID: string; + + /** + * The ID used as a cursor/offset when making a paginated request. + */ cursorID?: string | null; }; +/** + * + */ type PaginatedRequest = Request & PaginationConfig & { + /** + * A boolean flag to mark a request as Paginated. + */ isPaginated: true; }; From a52730085eecc19eb6026f2153cbd80f3e994ebb Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 09:58:11 -0600 Subject: [PATCH 063/512] Remove type casts from Onyx.connect --- src/libs/Middleware/Pagination.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 0f82823915aa..c5a633fb68a3 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -43,17 +43,15 @@ function registerPaginationConfig({ + key: config.resourceCollectionKey, waitForCollectionCallback: true, callback: (data) => { resources.set(config.resourceCollectionKey, data); }, }); - Onyx.connect({ - // TODO: Also not sure why this cast is needed. - key: config.pageCollectionKey as OnyxPagesKey, + Onyx.connect({ + key: config.pageCollectionKey, waitForCollectionCallback: true, callback: (data) => { pages.set(config.pageCollectionKey, data); From 64c8161d2e1429948bdd2825aab6b85c1c81172c Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 09:59:46 -0600 Subject: [PATCH 064/512] remove TODO: TODONOTHING --- src/libs/Middleware/Pagination.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index c5a633fb68a3..5920dfb444b3 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -39,7 +39,6 @@ function registerPaginationConfig): void { - // TODO: Is there a way to avoid these casts? paginationConfigs.set(initialCommand, {...config, type: 'initial'} as unknown as PaginationConfigMapValue); paginationConfigs.set(previousCommand, {...config, type: 'previous'} as unknown as PaginationConfigMapValue); paginationConfigs.set(nextCommand, {...config, type: 'next'} as unknown as PaginationConfigMapValue); From f9f44917d9de0a38c30cedb92bc39c09304007a7 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 10:35:01 -0600 Subject: [PATCH 065/512] Document Pages shape --- src/types/onyx/Pages.ts | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/types/onyx/Pages.ts b/src/types/onyx/Pages.ts index bc95c20c85aa..746e3330895d 100644 --- a/src/types/onyx/Pages.ts +++ b/src/types/onyx/Pages.ts @@ -1,3 +1,59 @@ +/** + * An array of arrays of IDs, representing pages of a resource fetched via pagination. + * + * Here's an example (assuming a page size of 5 and sequential IDs): + * + * 1. Open a report, fetch the latest reportActions. Pages would look like this: + * + * [ + * [ + * 11, + * 12, + * 13, + * 14, + * 15, + * ], + * ] + * + * 2. Click on a link to reportAction 7. Now Pages looks like this: + * + * [ + * [ + * 5, + * 6, + * 7, + * 8, + * 9, + * ], + * // This space between these non-continuous pages represents a gap that must be filled + * [ + * 11, + * 12, + * 13, + * 14, + * 15, + * ], + * ] + * + * 3. Scroll down, load more actions after reportAction 9 + * + * [ + * [ + * 5, + * 6, + * 7, + * 8, + * 9, + * // Note: the gap is filled and the pages are now continuous/merged together + * 10, + * 11, + * 12, + * 13, + * 14, + * 15, + * ], + * ] + */ type Pages = string[][]; export default Pages; From c0fcffb713ddabb6a36053c713cc36bcb92fc10c Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 10:36:55 -0600 Subject: [PATCH 066/512] Remove duplicate ReportActionsPages type --- src/pages/home/ReportScreen.tsx | 2 +- src/types/onyx/ReportActionsPages.ts | 3 --- src/types/onyx/index.ts | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 src/types/onyx/ReportActionsPages.ts diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index f19988e4f455..2155374894f9 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -76,7 +76,7 @@ type ReportScreenOnyxProps = { reportMetadata: OnyxEntry; /** Pagination data */ - pages: OnyxEntry; + pages: OnyxEntry; }; type OnyxHOCProps = { diff --git a/src/types/onyx/ReportActionsPages.ts b/src/types/onyx/ReportActionsPages.ts deleted file mode 100644 index 0737bd69c695..000000000000 --- a/src/types/onyx/ReportActionsPages.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ReportActionsPages = string[][]; - -export default ReportActionsPages; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d0ed400a173b..fe7bf311448d 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -59,7 +59,6 @@ import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; import type ReportActionsDrafts from './ReportActionsDrafts'; -import type ReportActionsPages from './ReportActionsPages'; import type ReportMetadata from './ReportMetadata'; import type ReportNameValuePairs from './ReportNameValuePairs'; import type ReportNextStep from './ReportNextStep'; @@ -146,7 +145,6 @@ export type { ReportActions, ReportActionsDraft, ReportActionsDrafts, - ReportActionsPages, ReportMetadata, ReportNextStep, Request, From 035e81c500ca9fe54577ebf278113cd9910f47e8 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 10:40:25 -0600 Subject: [PATCH 067/512] Suppress lint for global fetch any type --- tests/ui/UnreadIndicatorsTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 6144165275ea..919642a99926 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -120,6 +120,7 @@ beforeAll(() => { // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. + // @eslint-ignore-next-line @typescript-eslint/no-explicit-any global.fetch = TestHelper.getGlobalFetchMock() as any; Linking.setInitialURL('https://new.expensify.com/'); From c67511c920468d7ef026478d8e39b920eef92877 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 10:48:37 -0600 Subject: [PATCH 068/512] Revert TS bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9ee4be36f6b..afee5c604f16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -240,7 +240,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "4.20.0", - "typescript": "^5.4.5", + "typescript": "^5.3.2", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", diff --git a/package.json b/package.json index 50fd3cfabb3d..ed504878fc8f 100644 --- a/package.json +++ b/package.json @@ -292,7 +292,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "4.20.0", - "typescript": "^5.4.5", + "typescript": "^5.3.2", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", From bfa6dbd6989c8c237378b557a51f511331ac7d4f Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 11:05:03 -0600 Subject: [PATCH 069/512] Remove unnecessary optimistic previousReportActionID --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 33971530ec77..4b89c03ad5f5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4340,7 +4340,6 @@ function buildOptimisticActionableTrackExpenseWhisper(iouAction: OptimisticIOURe type: 'TEXT', }, ], - previousReportActionID: iouAction?.reportActionID, reportActionID, shouldShow: true, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, From fa30c876b3462678bcb7eb5e844c247f14a9e777 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 11:05:46 -0600 Subject: [PATCH 070/512] Remove unnecessary effect from ReportActionsView --- src/pages/home/report/ReportActionsView.tsx | 32 --------------------- 1 file changed, 32 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 29d948917c8e..0e6c3e9b3cbd 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -404,38 +404,6 @@ function ReportActionsView({ Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); }, [hasCachedActionOnFirstRender]); - useEffect(() => { - // TODO: Can this be deleted now? - // Temporary solution for handling REPORT_PREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP - // This code should be removed once REPORT_PREVIEW is no longer repositioned. - // We need to call openReport for gaps created by moving REPORT_PREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. - const shouldOpenReport = - newestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && - !hasCreatedAction && - isReadyForCommentLinking && - reportActions.length < 24 && - reportActions.length >= 1 && - !isLoadingInitialReportActions && - !isLoadingOlderReportActions && - !isLoadingNewerReportActions && - !ReportUtils.isInvoiceRoom(report); - - if (shouldOpenReport) { - Report.openReport(reportID, reportActionID); - } - }, [ - hasCreatedAction, - reportID, - reportActions, - reportActionID, - newestReportAction?.actionName, - isReadyForCommentLinking, - isLoadingOlderReportActions, - isLoadingNewerReportActions, - isLoadingInitialReportActions, - report, - ]); - // Check if the first report action in the list is the one we're currently linked to const isTheFirstReportActionIsLinked = newestReportAction?.reportActionID === reportActionID; From 63e3a745af3f0a5ede16f3eeca8d7ef82fb61cbc Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 11:23:06 -0600 Subject: [PATCH 071/512] Fix some type errors --- src/components/KeyboardAvoidingView/index.ios.tsx | 2 +- src/components/KeyboardAvoidingView/index.tsx | 2 +- src/components/KeyboardAvoidingView/types.ts | 3 --- src/libs/API/types.ts | 2 ++ src/libs/actions/ExitSurvey.ts | 3 ++- 5 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 src/components/KeyboardAvoidingView/types.ts diff --git a/src/components/KeyboardAvoidingView/index.ios.tsx b/src/components/KeyboardAvoidingView/index.ios.tsx index a7cd767377ef..1c5a31b62ebd 100644 --- a/src/components/KeyboardAvoidingView/index.ios.tsx +++ b/src/components/KeyboardAvoidingView/index.ios.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; -import type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from 'react-native'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx index 09ec21e5b219..090922e8acc8 100644 --- a/src/components/KeyboardAvoidingView/index.tsx +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {View} from 'react-native'; -import type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from 'react-native'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; diff --git a/src/components/KeyboardAvoidingView/types.ts b/src/components/KeyboardAvoidingView/types.ts deleted file mode 100644 index 48d354e8b53f..000000000000 --- a/src/components/KeyboardAvoidingView/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {KeyboardAvoidingViewProps} from 'react-native'; - -export default KeyboardAvoidingViewProps; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8e38f3c26d63..d7727853e7c6 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -550,6 +550,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages', JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink', RECONNECT_APP: 'ReconnectApp', + SWITCH_TO_OLD_DOT: 'SwitchToOldDot', } as const; type SideEffectRequestCommand = ValueOf; @@ -562,6 +563,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams; [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; + [SIDE_EFFECT_REQUEST_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/actions/ExitSurvey.ts b/src/libs/actions/ExitSurvey.ts index 67ac39d81bd6..12dc57f1dfde 100644 --- a/src/libs/actions/ExitSurvey.ts +++ b/src/libs/actions/ExitSurvey.ts @@ -1,6 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; import REASON_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm'; import type {ExitReason} from '@src/types/form/ExitSurveyReasonForm'; @@ -67,7 +68,7 @@ function switchToOldDot() { // eslint-disable-next-line rulesdir/no-api-side-effects-method return API.makeRequestWithSideEffects( - 'SwitchToOldDot', + SIDE_EFFECT_REQUEST_COMMANDS.SWITCH_TO_OLD_DOT, { reason: exitReason, surveyResponse: exitSurveyResponse, From 20f5bd6e399baf1922dcfd1db9789330c355eceb Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 11:26:37 -0600 Subject: [PATCH 072/512] Fix mockFetch types --- tests/ui/PaginationTest.tsx | 2 +- tests/ui/UnreadIndicatorsTest.tsx | 3 +-- tests/utils/TestHelper.ts | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index fc11693e588f..6eb7cf6c7891 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -105,7 +105,7 @@ jest.mock('@react-navigation/native', () => { } as typeof NativeNavigation; }); -const fetchMock = TestHelper.getGlobalFetchMock(); +const fetchMock = TestHelper.getGlobalFetchMock() as TestHelper.MockFetch; beforeAll(() => { global.fetch = fetchMock as unknown as typeof global.fetch; diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 919642a99926..41bc2ba913c1 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -120,8 +120,7 @@ beforeAll(() => { // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. - // @eslint-ignore-next-line @typescript-eslint/no-explicit-any - global.fetch = TestHelper.getGlobalFetchMock() as any; + global.fetch = TestHelper.getGlobalFetchMock(); Linking.setInitialURL('https://new.expensify.com/'); appSetup(); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index bbf2ec373353..102781cd07a2 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -8,7 +8,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; -type MockFetch = jest.Mock & { +type MockFetch = ReturnType & { pause: () => void; fail: () => void; succeed: () => void; @@ -217,7 +217,7 @@ function getGlobalFetchMock() { mockFetch.mockAPICommand = (command: string, response: OnyxResponse['onyxData']) => { responses.set(command, response); }; - return mockFetch; + return mockFetch as typeof fetch; } function setPersonalDetails(login: string, accountID: number) { From f7671bf946cffd996ea9afbf20a84c20925ac83c Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 11:29:36 -0600 Subject: [PATCH 073/512] Use regular for loop to iterate backwards without copy --- src/libs/PaginationUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 4f9be6c398f6..75725776a6c9 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -45,7 +45,8 @@ function findFirstItem(sortedItems: TResource[], page: string[], getI * Finds the id and index in sortedItems of the last item in the given page that's present in sortedItems. */ function findLastItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): ItemWithIndex | null { - for (const id of page.slice().reverse()) { + for (let i = page.length - 1; i > 0; i--) { + const id = page[i]; if (id === CONST.PAGINATION_END_ID) { return {id, index: sortedItems.length - 1}; } From f8dc2234f8d7856e46aa72dd8c2fc71297e4812d Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 12 Jun 2024 11:32:37 -0600 Subject: [PATCH 074/512] Update TS again --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f626736d074..35c42f6d84e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -241,7 +241,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "4.20.0", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", diff --git a/package.json b/package.json index ab98bb119a67..7aada96ce8e5 100644 --- a/package.json +++ b/package.json @@ -293,7 +293,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "4.20.0", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", From bd8910c38b37105f005c7feb85db6531928f6e68 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:00:44 +0200 Subject: [PATCH 075/512] Add Handle image zoom for mobile browser apps --- .../AttachmentCarousel/CarouselItem.tsx | 6 +- .../Attachments/AttachmentCarousel/index.tsx | 5 +- .../AttachmentViewImage/index.tsx | 6 +- .../Attachments/AttachmentView/index.tsx | 5 + src/components/ImageView/index.tsx | 200 ++++++++++++++++-- src/components/ImageView/types.ts | 3 + 6 files changed, 204 insertions(+), 21 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 2ec1883fd7de..c0b3714ff5d8 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -19,6 +19,9 @@ type CarouselItemProps = { /** onPress callback */ onPress?: () => void; + /** onClose callback */ + onClose?: () => void; + /** Whether attachment carousel modal is hovered over */ isModalHovered?: boolean; @@ -26,7 +29,7 @@ type CarouselItemProps = { isFocused: boolean; }; -function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) { +function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -77,6 +80,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr file={item.file} isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} + onClose={onClose} transactionID={item.transactionID} reportActionID={item.reportActionID} isHovered={isModalHovered} diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 947569538d32..f2ed12fc85eb 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -33,7 +33,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, onClose, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -174,6 +174,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ({item}: ListRenderItemInfo) => ( setShouldShowArrows((oldState) => !oldState) : undefined} @@ -181,7 +182,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, onClose, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index c195c1e34554..6756742a978c 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -15,14 +15,18 @@ type AttachmentViewImageProps = Pick void; + + /** Function for handle on close */ + onClose?: () => void; }; -function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) { +function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, onClose, isImage}: AttachmentViewImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( void; + /** Function for handle on close */ + onClose?: () => void | undefined; + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -84,6 +87,7 @@ function AttachmentView({ isFocused, isUsedInCarousel, isUsedInAttachmentModal, + onClose, isWorkspaceAvatar, maybeIcon, fallbackSource, @@ -220,6 +224,7 @@ function AttachmentView({ isAuthTokenRequired={isAuthTokenRequired} loadComplete={loadComplete} isImage={isImage} + onClose={onClose} onPress={onPress} onError={() => { setImageError(true); diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index f08941ef7d77..63ffa8fc93e0 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,16 +1,20 @@ import type {SyntheticEvent} from 'react'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; +import {SPRING_CONFIG} from '@components/MultiGestureCanvas/constants'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; @@ -18,7 +22,7 @@ import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; -function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipeDown}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -32,6 +36,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const [initialX, setInitialX] = useState(0); const [initialY, setInitialY] = useState(0); const [imgWidth, setImgWidth] = useState(0); + const [imgContainerHeight, setImgContainerHeight] = useState(0); + const [imgContainerWidth, setImgContainerWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); @@ -47,6 +53,131 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); setZoomScale(newZoomScale); }; + const scale = useSharedValue(1); + const deltaScale = useSharedValue(1); + const minScale = 1.0; + const maxScale = 20; + const translationX = useSharedValue(1); + const translationY = useSharedValue(1); + const prevTranslationX = useSharedValue(0); + const prevTranslationY = useSharedValue(0); + const zoomedContentWidth = useDerivedValue(() => imgContainerWidth * scale.value, [imgContainerWidth, scale.value]); + const zoomedContentHeight = useDerivedValue(() => imgContainerHeight * scale.value, [imgContainerHeight, scale.value]); + const maxTranslateX = useMemo(() => imgContainerWidth / 2, [imgContainerWidth]); + const maxTranslateY = useMemo(() => containerHeight / 2, [containerHeight]); + const horizontalBoundaries = useMemo(() => { + let horizontalBoundary = 0; + if (containerWidth < zoomedContentWidth.value) { + horizontalBoundary = Math.abs(containerWidth - zoomedContentWidth.value) / 2; + } + return {min: -horizontalBoundary, max: horizontalBoundary}; + }, [containerWidth, zoomedContentWidth.value]); + const verticalBoundaries = useMemo(() => { + let verticalBoundary = 0; + if (containerHeight < zoomedContentHeight.value) { + verticalBoundary = Math.abs(zoomedContentHeight.value - containerHeight) / 2; + } + return {min: -verticalBoundary, max: verticalBoundary}; + }, [containerHeight, zoomedContentHeight.value]); + const pinchGesture = Gesture.Pinch() + .onStart(() => { + deltaScale.value = scale.value; + }) + .onUpdate((e) => { + if (scale.value < minScale / 2) { + return; + } + scale.value = deltaScale.value * e.scale; + }) + .onEnd(() => { + if (scale.value < minScale) { + scale.value = withSpring(minScale, SPRING_CONFIG); + translationX.value = 0; + translationY.value = 0; + } + if (scale.value > maxScale) { + scale.value = withSpring(maxScale, SPRING_CONFIG); + } + deltaScale.value = scale.value; + }) + .runOnJS(true); + const clamp = (val: number, min: number, max: number) => { + 'worklet'; + + return Math.min(Math.max(val, min), max); + }; + const panGesture = Gesture.Pan() + .onStart(() => { + 'worklet'; + + prevTranslationX.value = translationX.value; + prevTranslationY.value = translationY.value; + }) + .onUpdate((e) => { + 'worklet'; + + if (scale.value === minScale) { + if (e.translationX === 0 && e.translationY > 0) { + translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); + } else { + return; + } + } + translationX.value = clamp(prevTranslationX.value + e.translationX, -maxTranslateX, maxTranslateX); + if (zoomedContentHeight.value < containerHeight) { + return; + } + translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); + }) + .onEnd(() => { + 'worklet'; + + const swipeDownPadding = 150; + const dy = translationY.value + swipeDownPadding; + if (dy >= maxTranslateY && scale.value === minScale) { + if (onSwipeDown) { + onSwipeDown(); + } + } else if (scale.value === minScale) { + translationY.value = withSpring(0, SPRING_CONFIG); + translationX.value = withSpring(0, SPRING_CONFIG); + return; + } + const tsx = translationX.value * scale.value; + const tsy = translationY.value * scale.value; + const inHorizontalBoundaries = tsx >= horizontalBoundaries.min && tsx <= horizontalBoundaries.max; + const inVerticalBoundaries = tsy >= verticalBoundaries.min && tsy <= verticalBoundaries.max; + if (!inHorizontalBoundaries) { + const halfx = zoomedContentWidth.value / 2; + const diffx = halfx - translationX.value * scale.value; + const valx = maxTranslateX - diffx; + if (valx > 0) { + const p = (translationX.value * scale.value - valx) / scale.value; + translationX.value = withSpring(p, SPRING_CONFIG); + } + if (valx < 0) { + const p = (translationX.value * scale.value - valx) / scale.value; + translationX.value = withSpring(-p, SPRING_CONFIG); + } + } + if (!inVerticalBoundaries) { + if (zoomedContentHeight?.value < containerHeight) { + return; + } + const halfy = zoomedContentHeight.value / 2; + const diffy = halfy - translationY.value * scale.value; + const valy = maxTranslateY - diffy; + if (valy > 0) { + const p = (translationY.value * scale.value - valy) / scale.value; + translationY.value = withSpring(p, SPRING_CONFIG); + } + if (valy < 0) { + const p = (translationY.value * scale.value - valy) / scale.value; + translationY.value = withSpring(-p, SPRING_CONFIG); + } + } + }) + .runOnJS(true); const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; @@ -195,25 +326,60 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}, {translateX: translationX.value}, {translateY: translationY.value}], + })); + + const imgContainerStyle = useMemo(() => { + if (imgWidth >= imgHeight || imgHeight < containerHeight) { + const imgStyle: ViewStyle[] = [{width: imgWidth < containerWidth ? '100%' : '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + return imgStyle; + } + if (imgHeight > imgWidth) { + const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + return imgStyle; + } + }, [imgWidth, imgHeight, containerWidth, containerHeight]); + if (canUseTouchScreen) { return ( - 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} - onLoadStart={imageLoadingStart} - onLoad={imageLoad} - onError={onError} - /> - {((isLoading && !isOffline) || (!isLoading && zoomScale === 0)) && } + + { + const {width, height} = e.nativeEvent.layout; + setImgContainerHeight(height); + setImgContainerWidth(width); + }} + > + { + const {width, height} = e.nativeEvent.source; + const params = { + nativeEvent: { + width, + height, + }, + }; + imageLoad(params); + }} + onError={onError} + /> + + + {isLoading && !isOffline && } {isLoading && } ); diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index b19e6b228cbd..aac6b994b5bc 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -14,6 +14,9 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; + /** Function to call when an user swipes down */ + onSwipeDown?: () => void; + /** Additional styles to add to the component */ style?: StyleProp; From 2bd1a2ae167ba58a6e1b8075b3f49e6c5bf75f45 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:11:54 +0200 Subject: [PATCH 076/512] Make a little refactoring --- src/components/ImageView/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 63ffa8fc93e0..59212a6fd317 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -331,15 +331,16 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe })); const imgContainerStyle = useMemo(() => { + const aspectRatio = (imgHeight && imgWidth / imgHeight) || 1; if (imgWidth >= imgHeight || imgHeight < containerHeight) { - const imgStyle: ViewStyle[] = [{width: imgWidth < containerWidth ? '100%' : '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + const imgStyle: ViewStyle[] = [{width: '100%', aspectRatio}]; return imgStyle; } if (imgHeight > imgWidth) { - const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio}]; return imgStyle; } - }, [imgWidth, imgHeight, containerWidth, containerHeight]); + }, [imgWidth, imgHeight, containerHeight]); if (canUseTouchScreen) { return ( From ca69b0fd30728774612e405cb780923348d38c0a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:42:11 +0200 Subject: [PATCH 077/512] Fix bug with close modal on send attachment screen --- src/components/AttachmentModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 54a073e30567..f0317dd904cb 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -547,6 +547,7 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} + onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} From 2e343765c5285ab19f0633ed3a61a95c1b547c00 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:46:25 +0200 Subject: [PATCH 078/512] Reset image position when onSwipeDown is undefined --- src/components/ImageView/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 59212a6fd317..deb44048d47e 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -137,6 +137,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe if (dy >= maxTranslateY && scale.value === minScale) { if (onSwipeDown) { onSwipeDown(); + } else { + translationY.value = withSpring(0, SPRING_CONFIG); + translationX.value = withSpring(0, SPRING_CONFIG); } } else if (scale.value === minScale) { translationY.value = withSpring(0, SPRING_CONFIG); From ef5f19b413715ca2c5931f7cc98d99fb6c24f4c8 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 13 Jun 2024 08:38:26 +0200 Subject: [PATCH 079/512] Fix ts error --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index b537d43e3df3..51d1df35f9ac 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -106,7 +106,7 @@ function ReportActionItemSingle({ } // If this is a report preview, display names and avatars of both people involved - const secondaryAvatar = ReportUtils.getSecondaryAvatar(report, iouReport ?? null, displayAllActors, isWorkspaceActor, actorAccountID); + const secondaryAvatar = ReportUtils.getSecondaryAvatar(report, iouReport, displayAllActors, isWorkspaceActor, actorAccountID); const primaryDisplayName = displayName; if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice From 58949066c8475b3faec960c5909ca7767cd742cb Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:21:02 +0200 Subject: [PATCH 080/512] Refactor code --- src/components/AttachmentModal.tsx | 34 +-- .../AttachmentCarousel/CarouselItem.tsx | 6 +- .../Pager/AttachmentCarouselPagerContext.ts | 15 +- .../Attachments/AttachmentCarousel/index.tsx | 14 +- .../Attachments/AttachmentCarousel/types.ts | 8 + .../AttachmentViewImage/index.tsx | 6 +- .../Attachments/AttachmentView/index.tsx | 5 - src/components/ImageView/index.tsx | 201 +----------------- src/components/MultiGestureCanvas/index.tsx | 4 +- 9 files changed, 65 insertions(+), 228 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f0317dd904cb..2f5c85b10fb3 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; -import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -185,6 +186,8 @@ function AttachmentModal({ const nope = useSharedValue(false); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); + const pagerRef = useRef(null); + const [zoomScale, setZoomScale] = useState(1); const [file, setFile] = useState( originalFileName @@ -469,11 +472,13 @@ function AttachmentModal({ () => ({ pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], activePage: 0, - pagerRef: undefined, + pagerRef, isPagerScrolling: nope, isScrollEnabled: nope, onTap: () => {}, - onScaleChanged: () => {}, + onScaleChanged: (value: number) => { + setZoomScale(value); + }, onSwipeDown: closeModal, }), [closeModal, nope, sourceForAttachmentView], @@ -528,15 +533,19 @@ function AttachmentModal({ )} {!shouldShowNotFoundPage && (!isEmptyObject(report) && !isReceiptAttachment ? ( - + + + ) : ( !!sourceForAttachmentView && shouldLoadAttachment && @@ -547,7 +556,6 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} - onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index c0b3714ff5d8..2ec1883fd7de 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -19,9 +19,6 @@ type CarouselItemProps = { /** onPress callback */ onPress?: () => void; - /** onClose callback */ - onClose?: () => void; - /** Whether attachment carousel modal is hovered over */ isModalHovered?: boolean; @@ -29,7 +26,7 @@ type CarouselItemProps = { isFocused: boolean; }; -function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: CarouselItemProps) { +function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -80,7 +77,6 @@ function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: Carou file={item.file} isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} - onClose={onClose} transactionID={item.transactionID} reportActionID={item.reportActionID} isHovered={isModalHovered} diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 87a9108d5f2e..c597b07487f0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,5 +1,6 @@ import type {ForwardedRef} from 'react'; import {createContext} from 'react'; +import type {GestureType} from 'react-native-gesture-handler'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; import type {AttachmentSource} from '@components/Attachments/types'; @@ -22,11 +23,23 @@ type AttachmentCarouselPagerContextValue = { /** The index of the active page */ activePage: number; - pagerRef?: ForwardedRef; + + /** The ref of the active attachment */ + pagerRef?: ForwardedRef; + + /** The scroll state of the attachment */ isPagerScrolling: SharedValue; + + /** The scroll active of the attachment */ isScrollEnabled: SharedValue; + + /** The function to call after tap */ onTap: () => void; + + /** The function to call after scale */ onScaleChanged: (scale: number) => void; + + /** The function to call after swipe down */ onSwipeDown: () => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f2ed12fc85eb..611e79622075 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,7 +1,9 @@ import isEqual from 'lodash/isEqual'; +import type {MutableRefObject} from 'react'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; import {Keyboard, PixelRatio, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated'; @@ -33,7 +35,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, onClose, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, pagerRef, zoomScale}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -174,7 +176,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ({item}: ListRenderItemInfo) => ( setShouldShowArrows((oldState) => !oldState) : undefined} @@ -182,13 +183,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /> ), - [activeSource, canUseTouchScreen, onClose, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( () => Gesture.Pan() - .enabled(canUseTouchScreen) + .enabled(canUseTouchScreen && zoomScale === 1) .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; @@ -205,8 +206,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } scrollTo(scrollRef, newIndex * cellWidth, 0, true); - }), - [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef], + }) + .withRef(pagerRef as MutableRefObject), + [attachments.length, canUseTouchScreen, cellWidth, page, pagerRef, scrollRef, zoomScale], ); return ( diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts index d31ebbd328cd..984f914dfd33 100644 --- a/src/components/Attachments/AttachmentCarousel/types.ts +++ b/src/components/Attachments/AttachmentCarousel/types.ts @@ -1,4 +1,6 @@ +import type {ForwardedRef} from 'react'; import type {ViewToken} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; @@ -38,6 +40,12 @@ type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & { /** A callback that is called when swipe-down-to-close gesture happens */ onClose: () => void; + + /** The ref of the pager */ + pagerRef: ForwardedRef; + + /** The zoom scale of the attachment */ + zoomScale?: number; }; export type {AttachmentCarouselProps, UpdatePageProps, AttachmentCaraouselOnyxProps}; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index 6756742a978c..c195c1e34554 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -15,18 +15,14 @@ type AttachmentViewImageProps = Pick void; - - /** Function for handle on close */ - onClose?: () => void; }; -function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, onClose, isImage}: AttachmentViewImageProps) { +function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( void; - /** Function for handle on close */ - onClose?: () => void | undefined; - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -87,7 +84,6 @@ function AttachmentView({ isFocused, isUsedInCarousel, isUsedInAttachmentModal, - onClose, isWorkspaceAvatar, maybeIcon, fallbackSource, @@ -224,7 +220,6 @@ function AttachmentView({ isAuthTokenRequired={isAuthTokenRequired} loadComplete={loadComplete} isImage={isImage} - onClose={onClose} onPress={onPress} onError={() => { setImageError(true); diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index deb44048d47e..fa8f5fba993e 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,20 +1,17 @@ import type {SyntheticEvent} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; import {View} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; -import {SPRING_CONFIG} from '@components/MultiGestureCanvas/constants'; +import Lightbox from '@components/Lightbox'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; @@ -22,7 +19,7 @@ import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; -function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipeDown}: ImageViewProps) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -36,8 +33,6 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe const [initialX, setInitialX] = useState(0); const [initialY, setInitialY] = useState(0); const [imgWidth, setImgWidth] = useState(0); - const [imgContainerHeight, setImgContainerHeight] = useState(0); - const [imgContainerWidth, setImgContainerWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); @@ -53,134 +48,6 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); setZoomScale(newZoomScale); }; - const scale = useSharedValue(1); - const deltaScale = useSharedValue(1); - const minScale = 1.0; - const maxScale = 20; - const translationX = useSharedValue(1); - const translationY = useSharedValue(1); - const prevTranslationX = useSharedValue(0); - const prevTranslationY = useSharedValue(0); - const zoomedContentWidth = useDerivedValue(() => imgContainerWidth * scale.value, [imgContainerWidth, scale.value]); - const zoomedContentHeight = useDerivedValue(() => imgContainerHeight * scale.value, [imgContainerHeight, scale.value]); - const maxTranslateX = useMemo(() => imgContainerWidth / 2, [imgContainerWidth]); - const maxTranslateY = useMemo(() => containerHeight / 2, [containerHeight]); - const horizontalBoundaries = useMemo(() => { - let horizontalBoundary = 0; - if (containerWidth < zoomedContentWidth.value) { - horizontalBoundary = Math.abs(containerWidth - zoomedContentWidth.value) / 2; - } - return {min: -horizontalBoundary, max: horizontalBoundary}; - }, [containerWidth, zoomedContentWidth.value]); - const verticalBoundaries = useMemo(() => { - let verticalBoundary = 0; - if (containerHeight < zoomedContentHeight.value) { - verticalBoundary = Math.abs(zoomedContentHeight.value - containerHeight) / 2; - } - return {min: -verticalBoundary, max: verticalBoundary}; - }, [containerHeight, zoomedContentHeight.value]); - const pinchGesture = Gesture.Pinch() - .onStart(() => { - deltaScale.value = scale.value; - }) - .onUpdate((e) => { - if (scale.value < minScale / 2) { - return; - } - scale.value = deltaScale.value * e.scale; - }) - .onEnd(() => { - if (scale.value < minScale) { - scale.value = withSpring(minScale, SPRING_CONFIG); - translationX.value = 0; - translationY.value = 0; - } - if (scale.value > maxScale) { - scale.value = withSpring(maxScale, SPRING_CONFIG); - } - deltaScale.value = scale.value; - }) - .runOnJS(true); - const clamp = (val: number, min: number, max: number) => { - 'worklet'; - - return Math.min(Math.max(val, min), max); - }; - const panGesture = Gesture.Pan() - .onStart(() => { - 'worklet'; - - prevTranslationX.value = translationX.value; - prevTranslationY.value = translationY.value; - }) - .onUpdate((e) => { - 'worklet'; - - if (scale.value === minScale) { - if (e.translationX === 0 && e.translationY > 0) { - translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); - } else { - return; - } - } - translationX.value = clamp(prevTranslationX.value + e.translationX, -maxTranslateX, maxTranslateX); - if (zoomedContentHeight.value < containerHeight) { - return; - } - translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); - }) - .onEnd(() => { - 'worklet'; - - const swipeDownPadding = 150; - const dy = translationY.value + swipeDownPadding; - if (dy >= maxTranslateY && scale.value === minScale) { - if (onSwipeDown) { - onSwipeDown(); - } else { - translationY.value = withSpring(0, SPRING_CONFIG); - translationX.value = withSpring(0, SPRING_CONFIG); - } - } else if (scale.value === minScale) { - translationY.value = withSpring(0, SPRING_CONFIG); - translationX.value = withSpring(0, SPRING_CONFIG); - return; - } - const tsx = translationX.value * scale.value; - const tsy = translationY.value * scale.value; - const inHorizontalBoundaries = tsx >= horizontalBoundaries.min && tsx <= horizontalBoundaries.max; - const inVerticalBoundaries = tsy >= verticalBoundaries.min && tsy <= verticalBoundaries.max; - if (!inHorizontalBoundaries) { - const halfx = zoomedContentWidth.value / 2; - const diffx = halfx - translationX.value * scale.value; - const valx = maxTranslateX - diffx; - if (valx > 0) { - const p = (translationX.value * scale.value - valx) / scale.value; - translationX.value = withSpring(p, SPRING_CONFIG); - } - if (valx < 0) { - const p = (translationX.value * scale.value - valx) / scale.value; - translationX.value = withSpring(-p, SPRING_CONFIG); - } - } - if (!inVerticalBoundaries) { - if (zoomedContentHeight?.value < containerHeight) { - return; - } - const halfy = zoomedContentHeight.value / 2; - const diffy = halfy - translationY.value * scale.value; - const valy = maxTranslateY - diffy; - if (valy > 0) { - const p = (translationY.value * scale.value - valy) / scale.value; - translationY.value = withSpring(p, SPRING_CONFIG); - } - if (valy < 0) { - const p = (translationY.value * scale.value - valy) / scale.value; - translationY.value = withSpring(-p, SPRING_CONFIG); - } - } - }) - .runOnJS(true); const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; @@ -329,63 +196,13 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}, {translateX: translationX.value}, {translateY: translationY.value}], - })); - - const imgContainerStyle = useMemo(() => { - const aspectRatio = (imgHeight && imgWidth / imgHeight) || 1; - if (imgWidth >= imgHeight || imgHeight < containerHeight) { - const imgStyle: ViewStyle[] = [{width: '100%', aspectRatio}]; - return imgStyle; - } - if (imgHeight > imgWidth) { - const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio}]; - return imgStyle; - } - }, [imgWidth, imgHeight, containerHeight]); - if (canUseTouchScreen) { return ( - - - { - const {width, height} = e.nativeEvent.layout; - setImgContainerHeight(height); - setImgContainerWidth(width); - }} - > - { - const {width, height} = e.nativeEvent.source; - const params = { - nativeEvent: { - width, - height, - }, - }; - imageLoad(params); - }} - onError={onError} - /> - - - {isLoading && !isOffline && } - {isLoading && } - + ); } return ( diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 31a1f7a2c3d8..2e5cf8018c70 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,6 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; import type PagerView from 'react-native-pager-view'; @@ -40,7 +41,7 @@ type MultiGestureCanvasProps = ChildrenProps & { shouldDisableTransformationGestures?: SharedValue; /** If there is a pager wrapping the canvas, we need to disable the pan gesture in case the pager is swiping */ - pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude + pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude /** Handles scale changed event */ onScaleChanged?: OnScaleChangedCallback; @@ -48,6 +49,7 @@ type MultiGestureCanvasProps = ChildrenProps & { /** Handles scale changed event */ onTap?: OnTapCallback; + /** Handles swipe down event */ onSwipeDown?: OnSwipeDownCallback; }; From e30a3bbc8e9c1040fb5b32740081f303ff21552d Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:29:45 +0200 Subject: [PATCH 081/512] Make some minnor changes --- src/components/AttachmentModal.tsx | 11 ++++++++--- .../Pager/AttachmentCarouselPagerContext.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 2f5c85b10fb3..5838577bcc5d 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -397,6 +397,13 @@ function AttachmentModal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [onModalClose]); + /** + * scale handler for attachment + */ + const scaleChanged = (value: number) => { + setZoomScale(value); + }; + /** * open the modal */ @@ -476,9 +483,7 @@ function AttachmentModal({ isPagerScrolling: nope, isScrollEnabled: nope, onTap: () => {}, - onScaleChanged: (value: number) => { - setZoomScale(value); - }, + onScaleChanged: scaleChanged, onSwipeDown: closeModal, }), [closeModal, nope, sourceForAttachmentView], diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index c597b07487f0..7af38f300532 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -18,28 +18,28 @@ type AttachmentCarouselPagerItems = { }; type AttachmentCarouselPagerContextValue = { - /** The list of items that are shown in the pager */ + /** List of items displayed in the attachment */ pagerItems: AttachmentCarouselPagerItems[]; - /** The index of the active page */ + /** Index of the currently active page */ activePage: number; - /** The ref of the active attachment */ + /** Ref to the active attachment */ pagerRef?: ForwardedRef; - /** The scroll state of the attachment */ + /** Indicates if the pager is currently scrolling */ isPagerScrolling: SharedValue; - /** The scroll active of the attachment */ + /** Indicates if scrolling is enabled for the attachment */ isScrollEnabled: SharedValue; - /** The function to call after tap */ + /** Function to call after a tap event */ onTap: () => void; - /** The function to call after scale */ + /** Function to call when the scale changes */ onScaleChanged: (scale: number) => void; - /** The function to call after swipe down */ + /** Function to call after a swipe down event */ onSwipeDown: () => void; }; From 2f40b541ad23de47838eca6cf9fb6d91cfc9c7ea Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:58:04 +0200 Subject: [PATCH 082/512] Update animation for changing attachment item --- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 611e79622075..ebb43ef45eb9 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -190,7 +190,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, () => Gesture.Pan() .enabled(canUseTouchScreen && zoomScale === 1) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX * 2, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; if (velocityX > MIN_FLING_VELOCITY) { From ebb0eb4d3c8989e0d5e1fb2665fb46897f9ac4db Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 13:33:16 +0200 Subject: [PATCH 083/512] Remove unnecessary type --- src/components/ImageView/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index aac6b994b5bc..b19e6b228cbd 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -14,9 +14,6 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; - /** Function to call when an user swipes down */ - onSwipeDown?: () => void; - /** Additional styles to add to the component */ style?: StyleProp; From 62ef24417b2170691bc02b45e9f57abe9b0fcbda Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 13 Jun 2024 15:14:44 +0200 Subject: [PATCH 084/512] fetching onyx store key/value pairs PoC --- .../Troubleshoot/TroubleshootPage.tsx | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 34d75d9279ad..a895cf61e3b2 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import ClientSideLoggingToolMenu from '@components/ClientSideLoggingToolMenu'; @@ -26,6 +26,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import type {TranslationPaths} from '@src/languages/types'; +import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -53,8 +54,51 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); + const getOnyxKeys = (keysObject: Record) => { + const keys: string[] = []; + + Object.keys(keysObject).forEach((key) => { + if (typeof keysObject[key] === 'object') { + keys.push(...getOnyxKeys(keysObject[key] as Record)); + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + keys.push(keysObject[key] as string); + }); + + return keys; + } + + const getOnyxValues = () => { + const keys = getOnyxKeys(ONYXKEYS); + const promises: Array>> = []; + + keys.forEach((key) => { + promises.push(new Promise((resolve) => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + const connectionID = Onyx.connect({ + key: key as OnyxKey, + callback: (value) => { + if (!value) { + resolve(null); + return; + } + + resolve({key, value}); + Onyx.disconnect(connectionID); + }, + }); + })); + }); + + return Promise.all(promises); + }; + const exportOnyxState = () => { - console.log('export state here'); + getOnyxValues().then((value) => { + console.log('exported onyx state: ', value.filter(Boolean)); + }); }; const menuItems = useMemo(() => { From 95e118b39567ad95b4dd664778483029b36738c5 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 13 Jun 2024 15:37:15 +0200 Subject: [PATCH 085/512] direct access to indexedDB instance possibility added --- .../Troubleshoot/TroubleshootPage.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index a895cf61e3b2..b622dc81ed9d 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -95,7 +95,39 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { return Promise.all(promises); }; + const readFromIndexedDB = () => new Promise((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open('OnyxDB', 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction('keyvaluepairs'); + const objectStore = transaction.objectStore('keyvaluepairs'); + const cursor = objectStore.openCursor(); + + const queryResult: Record = {}; + + cursor.onerror = () => { + console.error('Error reading cursor'); + } + + cursor.onsuccess = (event) => { + const { result } = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } + else { + resolve(queryResult); + } + } + }; + }); + const exportOnyxState = () => { + readFromIndexedDB().then((value) => { + console.log('exported indexedDB state: ', value); + }); + getOnyxValues().then((value) => { console.log('exported onyx state: ', value.filter(Boolean)); }); From fca2b12db0c625967fb9725ce774c8924e009b27 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 13 Jun 2024 16:01:04 +0200 Subject: [PATCH 086/512] distinction between web and native way of storing Onyx data taken into account --- src/libs/ExportOnyxState/index.native.ts | 19 +++++++++++ src/libs/ExportOnyxState/index.ts | 31 +++++++++++++++++ .../Troubleshoot/TroubleshootPage.tsx | 33 ++----------------- 3 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 src/libs/ExportOnyxState/index.native.ts create mode 100644 src/libs/ExportOnyxState/index.ts diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts new file mode 100644 index 000000000000..3b8b019a32c0 --- /dev/null +++ b/src/libs/ExportOnyxState/index.native.ts @@ -0,0 +1,19 @@ +import {open} from 'react-native-quick-sqlite'; + +const readFromIndexedDB = () => new Promise((resolve) => { + const db = open({name: 'OnyxDB'}); + const query = 'SELECT * FROM keyvaluepairs'; + + db.executeAsync(query, []).then(({rows}) => { + // eslint-disable-next-line no-underscore-dangle + const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); + + resolve(result); + }); + + db.close(); +}); + +export default { + readFromIndexedDB, +} diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts new file mode 100644 index 000000000000..6b61143f9e74 --- /dev/null +++ b/src/libs/ExportOnyxState/index.ts @@ -0,0 +1,31 @@ +const readFromIndexedDB = () => new Promise((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open('OnyxDB', 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction('keyvaluepairs'); + const objectStore = transaction.objectStore('keyvaluepairs'); + const cursor = objectStore.openCursor(); + + const queryResult: Record = {}; + + cursor.onerror = () => { + console.error('Error reading cursor'); + } + + cursor.onsuccess = (event) => { + const { result } = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } + else { + resolve(queryResult); + } + } + }; +}); + +export default { + readFromIndexedDB, +} diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index b622dc81ed9d..e80f1a8b3e79 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; -import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; +import Onyx, {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import ClientSideLoggingToolMenu from '@components/ClientSideLoggingToolMenu'; @@ -31,6 +31,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; +import ExportOnyxState from '@libs/ExportOnyxState'; type BaseMenuItem = { translationKey: TranslationPaths; @@ -95,36 +96,8 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { return Promise.all(promises); }; - const readFromIndexedDB = () => new Promise((resolve) => { - let db: IDBDatabase; - const openRequest = indexedDB.open('OnyxDB', 1); - openRequest.onsuccess = () => { - db = openRequest.result; - const transaction = db.transaction('keyvaluepairs'); - const objectStore = transaction.objectStore('keyvaluepairs'); - const cursor = objectStore.openCursor(); - - const queryResult: Record = {}; - - cursor.onerror = () => { - console.error('Error reading cursor'); - } - - cursor.onsuccess = (event) => { - const { result } = event.target as IDBRequest; - if (result) { - queryResult[result.primaryKey as string] = result.value; - result.continue(); - } - else { - resolve(queryResult); - } - } - }; - }); - const exportOnyxState = () => { - readFromIndexedDB().then((value) => { + ExportOnyxState.readFromIndexedDB().then((value) => { console.log('exported indexedDB state: ', value); }); From cb796cdc7bfc779b9e98bbec1c713d04a11530d3 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 13 Jun 2024 14:14:16 -0400 Subject: [PATCH 087/512] Fix mock --- __mocks__/react-native.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index e884a288aefb..4a9052148d82 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -86,7 +86,7 @@ jest.doMock('react-native', () => { }, Dimensions: { ...ReactNative.Dimensions, - addEventListener: jest.fn(), + addEventListener: jest.fn(() => ({remove: jest.fn()})), get: () => dimensions, set: (newDimensions: Record) => { dimensions = newDimensions; From 8b40d840e99c33792c79b83291ff646b3c99553b Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Thu, 13 Jun 2024 23:17:11 +0200 Subject: [PATCH 088/512] Remove file that was readded during main merge --- .../Profile/PersonalDetails/AddressPage.tsx | 153 ------------------ 1 file changed, 153 deletions(-) delete mode 100644 src/pages/settings/Profile/PersonalDetails/AddressPage.tsx diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx b/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx deleted file mode 100644 index 91a8b94537ab..000000000000 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import AddressForm from '@components/AddressForm'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import type {FormOnyxValues} from '@src/components/Form/types'; -import CONST from '@src/CONST'; -import type {Country} from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {PrivatePersonalDetails} from '@src/types/onyx'; -import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; - -type AddressPageOnyxProps = { - /** User's private personal details */ - privatePersonalDetails: OnyxEntry; - /** Whether app is loading */ - isLoadingApp: OnyxEntry; -}; - -type AddressPageProps = StackScreenProps & AddressPageOnyxProps; - -/** - * Submit form to update user's first and last legal name - * @param values - form input values - */ -function updateAddress(values: FormOnyxValues) { - PersonalDetails.updateAddress( - values.addressLine1?.trim() ?? '', - values.addressLine2?.trim() ?? '', - values.city.trim(), - values.state.trim(), - values?.zipPostCode?.trim().toUpperCase() ?? '', - values.country, - ); -} - -function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: AddressPageProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const address = useMemo(() => privatePersonalDetails?.address, [privatePersonalDetails]); - const countryFromUrlTemp = route?.params?.country; - - // Check if country is valid - const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; - const stateFromUrl = useGeographicalStateFromRoute(); - const [currentCountry, setCurrentCountry] = useState(address?.country); - const [street1, street2] = (address?.street ?? '').split('\n'); - const [state, setState] = useState(address?.state); - const [city, setCity] = useState(address?.city); - const [zipcode, setZipcode] = useState(address?.zip); - - useEffect(() => { - if (!address) { - return; - } - setState(address.state); - setCurrentCountry(address.country); - setCity(address.city); - setZipcode(address.zip); - }, [address]); - - const handleAddressChange = useCallback((value: unknown, key: unknown) => { - const addressPart = value as string; - const addressPartKey = key as keyof Address; - - if (addressPartKey !== 'country' && addressPartKey !== 'state' && addressPartKey !== 'city' && addressPartKey !== 'zipPostCode') { - return; - } - if (addressPartKey === 'country') { - setCurrentCountry(addressPart as Country | ''); - setState(''); - setCity(''); - setZipcode(''); - return; - } - if (addressPartKey === 'state') { - setState(addressPart); - setCity(''); - setZipcode(''); - return; - } - if (addressPartKey === 'city') { - setCity(addressPart); - setZipcode(''); - return; - } - setZipcode(addressPart); - }, []); - - useEffect(() => { - if (!countryFromUrl) { - return; - } - handleAddressChange(countryFromUrl, 'country'); - }, [countryFromUrl, handleAddressChange]); - - useEffect(() => { - if (!stateFromUrl) { - return; - } - handleAddressChange(stateFromUrl, 'state'); - }, [handleAddressChange, stateFromUrl]); - - return ( - - Navigation.goBack()} - /> - {isLoadingApp ? ( - - ) : ( - - )} - - ); -} - -AddressPage.displayName = 'AddressPage'; - -export default withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, -})(AddressPage); From db1aa8a488fe60f4bb73936c860ec01ba774586e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 14 Jun 2024 10:27:52 +0200 Subject: [PATCH 089/512] Minor fix --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1683ef3e3df2..55cd658b4afd 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -139,7 +139,7 @@ function IOURequestStepConfirmation({ const participants = useMemo( () => transaction?.participants?.map((participant) => { - const participantAccountID = participant.accountID ?? -1; + const participantAccountID = participant.accountID; if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { return participant; From d017bf560a9742c413271c03b2bb5c895b29416a Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 14 Jun 2024 12:03:50 +0200 Subject: [PATCH 090/512] Reintroduce old fix --- src/pages/AddressPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/AddressPage.tsx b/src/pages/AddressPage.tsx index 90711ebbab92..852c57595b70 100644 --- a/src/pages/AddressPage.tsx +++ b/src/pages/AddressPage.tsx @@ -42,7 +42,8 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true}: Addre setCurrentCountry(address.country); setCity(address.city); setZipcode(address.zip); - }, [address]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address?.state, address?.country, address?.city, address?.zip]); const handleAddressChange = useCallback((value: unknown, key: unknown) => { const addressPart = value as string; From 7eed2813dddc08e9e54b365e557745c218fc3b8b Mon Sep 17 00:00:00 2001 From: burczu Date: Fri, 14 Jun 2024 13:24:17 +0200 Subject: [PATCH 091/512] saving as txt file added --- src/libs/ExportOnyxState/index.native.ts | 25 +++++++++- src/libs/ExportOnyxState/index.ts | 15 ++++++ .../Troubleshoot/TroubleshootPage.tsx | 50 ++----------------- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index 3b8b019a32c0..2d53b9c39b5f 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,4 +1,6 @@ import {open} from 'react-native-quick-sqlite'; +import RNFS from "react-native-fs"; +import Share from "react-native-share"; const readFromIndexedDB = () => new Promise((resolve) => { const db = open({name: 'OnyxDB'}); @@ -9,11 +11,32 @@ const readFromIndexedDB = () => new Promise((resolve) => { const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); resolve(result); + db.close(); }); - db.close(); }); +// eslint-disable-next-line @lwc/lwc/no-async-await +const shareAsFile = async (value: string) => { + try { + // Define new filename and path for the app info file + const infoFileName = `onyx-state.txt`; + const infoFilePath = `${RNFS.DocumentDirectoryPath}/${infoFileName}`; + const actualInfoFile = `file://${infoFilePath}`; + + await RNFS.writeFile(infoFilePath, value, 'utf8'); + + const shareOptions = { + urls: [actualInfoFile], + }; + + await Share.open(shareOptions); + } catch (error) { + console.error('Error renaming and sharing file:', error); + } +} + export default { readFromIndexedDB, + shareAsFile, } diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 6b61143f9e74..e7427b097ae2 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -26,6 +26,21 @@ const readFromIndexedDB = () => new Promise((resolve) => { }; }); +// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await +const shareAsFile = async (value: string) => { + const element = document.createElement('a'); + element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(value)}`); + element.setAttribute('download', 'onyx-state.txt'); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + export default { readFromIndexedDB, + shareAsFile, } diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index e80f1a8b3e79..8b9e174697e5 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -26,12 +26,11 @@ import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import type {TranslationPaths} from '@src/languages/types'; -import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; import ExportOnyxState from '@libs/ExportOnyxState'; +import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; type BaseMenuItem = { translationKey: TranslationPaths; @@ -55,54 +54,13 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); - const getOnyxKeys = (keysObject: Record) => { - const keys: string[] = []; - - Object.keys(keysObject).forEach((key) => { - if (typeof keysObject[key] === 'object') { - keys.push(...getOnyxKeys(keysObject[key] as Record)); - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - keys.push(keysObject[key] as string); - }); - - return keys; - } - - const getOnyxValues = () => { - const keys = getOnyxKeys(ONYXKEYS); - const promises: Array>> = []; - - keys.forEach((key) => { - promises.push(new Promise((resolve) => { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs - const connectionID = Onyx.connect({ - key: key as OnyxKey, - callback: (value) => { - if (!value) { - resolve(null); - return; - } - - resolve({key, value}); - Onyx.disconnect(connectionID); - }, - }); - })); - }); - - return Promise.all(promises); - }; - const exportOnyxState = () => { ExportOnyxState.readFromIndexedDB().then((value) => { console.log('exported indexedDB state: ', value); - }); - getOnyxValues().then((value) => { - console.log('exported onyx state: ', value.filter(Boolean)); + ExportOnyxState.shareAsFile(JSON.stringify(value)).then(() => { + console.log('exported indexedDB state as file'); + }); }); }; From aa9bd53c3907b4371c9cc7c56f43428b52c74bbf Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 14 Jun 2024 15:34:25 +0200 Subject: [PATCH 092/512] Hide Pay as individual option in B2B room --- src/components/SettlementButton.tsx | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index b1386e1a6fa3..de1197c78f32 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -202,20 +202,22 @@ function SettlementButton({ } if (isInvoiceReport) { - buttonOptions.push({ - text: translate('iou.settlePersonal', {formattedAmount}), - icon: Expensicons.User, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - backButtonText: translate('iou.individual'), - subMenuItems: [ - { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.Cash, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), - }, - ], - }); + if (ReportUtils.isIndividualInvoiceRoom(chatReport)) { + buttonOptions.push({ + text: translate('iou.settlePersonal', {formattedAmount}), + icon: Expensicons.User, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.individual'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), + }, + ], + }); + } if (PolicyUtils.isPolicyAdmin(primaryPolicy)) { buttonOptions.push({ @@ -246,7 +248,7 @@ function SettlementButton({ return buttonOptions; // We don't want to reorder the options when the preferred payment method changes while the button is still visible // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); + }, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { From fec109e91133849daedd3c52eb4b03010242802a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 14 Jun 2024 21:05:55 +0200 Subject: [PATCH 093/512] Fix minnor issues related with arrows --- .../Attachments/AttachmentCarousel/index.tsx | 13 ++++++++--- .../AttachmentCarousel/useCarouselArrows.ts | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index ebb43ef45eb9..99332e703790 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -53,7 +53,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [page, setPage] = useState(0); const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); - const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); + const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows, onChangeArrowsState} = useCarouselArrows(); + + useEffect(() => { + if (!canUseTouchScreen || zoomScale !== 1) { + return; + } + setShouldShowArrows(true); + }, [canUseTouchScreen, page, setShouldShowArrows, zoomScale]); const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]); @@ -178,12 +185,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setShouldShowArrows((oldState) => !oldState) : undefined} + onPress={canUseTouchScreen ? () => onChangeArrowsState(zoomScale === 1) : undefined} isModalHovered={shouldShowArrows} /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, zoomScale, onChangeArrowsState, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts index 12ca3db4e2ff..1b21a3af6000 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts +++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts @@ -7,6 +7,7 @@ function useCarouselArrows() { const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen); const autoHideArrowTimeout = useRef(null); + const singleTapRef = useRef(null); /** * Cancels the automatic hiding of the arrows. @@ -45,7 +46,27 @@ function useCarouselArrows() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows}; + const onChangeArrowsState = useCallback( + (enabled: boolean) => { + if (!enabled) { + return; + } + + if (singleTapRef.current) { + clearTimeout(singleTapRef.current); + singleTapRef.current = null; + return; + } + + singleTapRef.current = setTimeout(() => { + setShouldShowArrows((oldState) => !oldState); + singleTapRef.current = null; + }, 200); + }, + [setShouldShowArrows], + ); + + return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows, onChangeArrowsState}; } export default useCarouselArrows; From 6c6390b443aadf6c863b06f907dace581357af4f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 14 Jun 2024 22:14:59 +0200 Subject: [PATCH 094/512] Fix bug with animation for carousel --- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- src/components/MultiGestureCanvas/usePanGesture.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 99332e703790..35accf837caa 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -197,7 +197,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, () => Gesture.Pan() .enabled(canUseTouchScreen && zoomScale === 1) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX * 2, 0, false)) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; if (velocityX > MIN_FLING_VELOCITY) { diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 903f384dd525..c236393027ef 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -206,10 +206,6 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; - if (!isSwipingDownToClose.value) { - panTranslateX.value += evt.changeX; - } - if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { panTranslateY.value += evt.changeY; } From 6cb459c135bea40e434877df7ed6cfe631178fd9 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 14 Jun 2024 18:02:34 -0400 Subject: [PATCH 095/512] Work on ui tests --- __mocks__/@react-navigation/native/index.ts | 42 ++- jest/setup.ts | 17 + package-lock.json | 39 ++- package.json | 2 + tests/ui/PaginationTest.tsx | 328 ++++++++------------ tests/ui/UnreadIndicatorsTest.tsx | 120 +------ tests/utils/TestHelper.ts | 77 +++-- tests/utils/debug.ts | 114 +++++++ 8 files changed, 390 insertions(+), 349 deletions(-) create mode 100644 tests/utils/debug.ts diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 0b7dda4621ad..747b6761fd6d 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,9 +1,41 @@ -import {useIsFocused as realUseIsFocused, useTheme as realUseTheme} from '@react-navigation/native'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +const realReactNavigation = jest.requireActual('@react-navigation/native'); // We only want these mocked for storybook, not jest -const useIsFocused: typeof realUseIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true; +const useIsFocused = process.env.NODE_ENV === 'test' ? realReactNavigation.useIsFocused : () => true; -const useTheme = process.env.NODE_ENV === 'test' ? realUseTheme : () => ({}); +const useTheme = process.env.NODE_ENV === 'test' ? realReactNavigation.useTheme : () => ({}); -export * from '@react-navigation/core'; -export {useIsFocused, useTheme}; +type Listener = () => void; + +const transitionEndListeners: Listener[] = []; + +const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); +}; + +const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; +}); + +const useNavigation = () => ({ + navigate: jest.fn(), + ...realReactNavigation.useNavigation, + getState: () => ({ + routes: [], + }), + addListener, +}); + +module.exports = { + ...realReactNavigation, + useIsFocused, + useTheme, + useNavigation, + triggerTransitionEnd, +}; diff --git a/jest/setup.ts b/jest/setup.ts index 416306ce8426..bf95cb208568 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -1,6 +1,7 @@ import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; +import type Animated from 'react-native-reanimated'; import 'setimmediate'; import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; @@ -20,6 +21,16 @@ jest.mock('react-native-onyx/dist/storage', () => mockStorage); // Mock NativeEventEmitter as it is needed to provide mocks of libraries which include it jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter'); +// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest +jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + ignoreLogs: jest.fn(), + ignoreAllLogs: jest.fn(), + }, +})); + // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params: string[]) => { if (params[0].startsWith('Timing:')) { @@ -53,3 +64,9 @@ jest.mock('react-native-sound', () => { jest.mock('react-native-share', () => ({ default: jest.fn(), })); + +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + createAnimatedPropAdapter: jest.fn, + useReducedMotion: jest.fn, +})); diff --git a/package-lock.json b/package-lock.json index 35c42f6d84e0..26de5d653ec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", "react-fast-pdf": "1.0.13", + "react-is": "18.2.0", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -180,6 +181,7 @@ "@types/react-beautiful-dnd": "^13.1.4", "@types/react-collapse": "^5.0.1", "@types/react-dom": "^18.2.4", + "@types/react-is": "18.2.0", "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", @@ -9212,6 +9214,11 @@ "react": "*" } }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/@react-navigation/devtools": { "version": "6.0.10", "dev": true, @@ -12616,6 +12623,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-native": { "version": "0.73.0", "deprecated": "This is a stub types definition. react-native provides its own type definitions, so you do not need this installed.", @@ -22756,6 +22772,11 @@ "react-is": "^16.7.0" } }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hosted-git-info": { "version": "4.1.0", "dev": true, @@ -30870,10 +30891,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "18.2.0", - "license": "MIT" - }, "node_modules/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -30951,6 +30968,11 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/propagate": { "version": "2.0.1", "license": "MIT", @@ -31596,8 +31618,9 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-map-gl": { "version": "7.1.3", @@ -32576,10 +32599,6 @@ "react": "^18.2.0" } }, - "node_modules/react-test-renderer/node_modules/react-is": { - "version": "18.2.0", - "license": "MIT" - }, "node_modules/react-test-renderer/node_modules/scheduler": { "version": "0.23.0", "license": "MIT", diff --git a/package.json b/package.json index 7aada96ce8e5..d5f9be31a486 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "process": "^0.11.10", "pusher-js": "8.3.0", "react": "18.2.0", + "react-is": "18.2.0", "react-beautiful-dnd": "^13.1.1", "react-collapse": "^5.1.0", "react-content-loader": "^7.0.0", @@ -229,6 +230,7 @@ "@types/node": "^20.11.5", "@types/pusher-js": "^5.1.0", "@types/react": "18.2.45", + "@types/react-is": "18.2.0", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-collapse": "^5.0.1", "@types/react-dom": "^18.2.4", diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 6eb7cf6c7891..97b9ba885870 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -1,21 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type * as NativeNavigation from '@react-navigation/native'; +import * as NativeNavigation from '@react-navigation/native'; import {act, fireEvent, render, screen} from '@testing-library/react-native'; import {addSeconds, format, subMinutes} from 'date-fns'; import React from 'react'; -import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; -import type Animated from 'react-native-reanimated'; +import type {ApiCommand} from '@libs/API/types'; import * as Localize from '@libs/Localize'; -import * as Pusher from '@libs/Pusher/pusher'; -import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as AppActions from '@userActions/App'; import * as User from '@userActions/User'; import App from '@src/App'; -import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import appSetup from '@src/setup'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -24,104 +19,23 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(30000); +jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); jest.mock('../../src/components/ConfirmedRoute.tsx'); -// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest -jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ - __esModule: true, - default: { - ignoreLogs: jest.fn(), - ignoreAllLogs: jest.fn(), - }, -})); - -jest.mock('react-native-reanimated', () => ({ - ...jest.requireActual('react-native-reanimated/mock'), - createAnimatedPropAdapter: jest.fn, - useReducedMotion: jest.fn, -})); +TestHelper.setupApp(); +const fetchMock = TestHelper.setupGlobalFetchMock(); -/** - * We need to keep track of the transitionEnd callback so we can trigger it in our tests - */ -let transitionEndCB: () => void; - -type ListenerMock = { - triggerTransitionEnd: () => void; - addListener: jest.Mock; +const LIST_SIZE = { + width: 300, + height: 400, }; - -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. - * - * @returns An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = (): ListenerMock => { - const transitionEndListeners: Array<() => void> = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener: jest.Mock = jest.fn().mockImplementation((listener: string, callback: () => void) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; +const LIST_CONTENT_SIZE = { + width: 300, + height: 600, }; -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - const {triggerTransitionEnd, addListener} = createAddListenerMock(); - transitionEndCB = triggerTransitionEnd; - - const useNavigation = () => - ({ - navigate: jest.fn(), - ...actualNav.useNavigation, - getState: () => ({ - routes: [], - }), - addListener, - setParams: jest.fn(), - } as typeof NativeNavigation.useNavigation); - - return { - ...actualNav, - useNavigation, - getState: () => ({ - routes: [], - }), - } as typeof NativeNavigation; -}); - -const fetchMock = TestHelper.getGlobalFetchMock() as TestHelper.MockFetch; - -beforeAll(() => { - global.fetch = fetchMock as unknown as typeof global.fetch; - - Linking.setInitialURL('https://new.expensify.com/'); - appSetup(); - - // Connect to Pusher - PusherConnectionManager.init(); - Pusher.init({ - appKey: CONFIG.PUSHER.APP_KEY, - cluster: CONFIG.PUSHER.CLUSTER, - authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, - }); -}); - function scrollToOffset(offset: number) { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); fireEvent.scroll(screen.getByLabelText(hintText), { @@ -129,59 +43,31 @@ function scrollToOffset(offset: number) { contentOffset: { y: offset, }, - contentSize: { - // Dimensions of the scrollable content - height: 500, - width: 100, - }, - layoutMeasurement: { - // Dimensions of the device - height: 700, - width: 300, - }, + contentSize: LIST_CONTENT_SIZE, + layoutMeasurement: LIST_SIZE, }, }); } -function getReportActions() { - const messageHintText = Localize.translateLocal('accessibilityHints.chatMessage'); - return screen.queryAllByLabelText(messageHintText); -} - function triggerListLayout() { fireEvent(screen.getByTestId('report-actions-view-container'), 'onLayout', { nativeEvent: { layout: { x: 0, y: 0, - width: 300, - height: 300, - }, - }, - }); - fireEvent(screen.getByTestId('report-actions-list'), 'onLayout', { - nativeEvent: { - layout: { - x: 0, - y: 0, - width: 300, - height: 300, + ...LIST_SIZE, }, }, }); + fireEvent(screen.getByTestId('report-actions-list'), 'onContentSizeChange', LIST_CONTENT_SIZE.width, LIST_CONTENT_SIZE.height); +} - getReportActions().forEach((e, i) => - fireEvent(e, 'onLayout', { - nativeEvent: { - layout: { - x: 0, - y: i * 100, - width: 300, - height: 100, - }, - }, - }), - ); +function getReportActions() { + return [ + ...screen.queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatMessage')), + // Created action has a different accessibility label. + ...screen.queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatWelcomeMessage')), + ]; } async function navigateToSidebarOption(index: number): Promise { @@ -189,7 +75,7 @@ async function navigateToSidebarOption(index: number): Promise { const optionRows = screen.queryAllByAccessibilityHint(hintText); fireEvent(optionRows[index], 'press'); await act(() => { - transitionEndCB?.(); + (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd(); }); // ReportScreen relies on the onLayout event to receive updates from onyx. triggerListLayout(); @@ -202,50 +88,83 @@ const USER_A_EMAIL = 'user_a@test.com'; const USER_B_ACCOUNT_ID = 2; const USER_B_EMAIL = 'user_b@test.com'; +function mockOpenReport(messageCount: number, includeCreatedAction: boolean) { + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const actions = Object.fromEntries( + Array.from({length: messageCount}).map((_, index) => { + const created = format(addSeconds(TEN_MINUTES_AGO, 10 * index), CONST.DATE.FNS_DB_FORMAT_STRING); + return [ + `${index + 1}`, + index === 0 && includeCreatedAction + ? { + reportActionID: '1', + actionName: 'CREATED' as const, + created, + message: [ + { + type: 'TEXT', + text: 'CREATED', + }, + ], + } + : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${index + 1}`), + ]; + }), + ); + fetchMock.mockAPICommand('OpenReport', [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + value: actions, + }, + ]); +} + +function expectAPICommandToHaveBeenCalled(commandName: ApiCommand, expectedCalls: number) { + expect(fetchMock.mock.calls.filter((c) => c[0] === `https://www.expensify.com.dev/api/${commandName}?`)).toHaveLength(expectedCalls); +} + /** * Sets up a test with a logged in user. Returns the test instance. */ -function signInAndGetApp(): Promise { +async function signInAndGetApp(): Promise { // Render the App and sign in as a test user. render(); - return waitForBatchedUpdatesWithAct() - .then(async () => { - await waitForBatchedUpdatesWithAct(); - const hintText = Localize.translateLocal('loginForm.loginForm'); - const loginForm = screen.queryAllByLabelText(hintText); - expect(loginForm).toHaveLength(1); - - await act(async () => { - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - }); - return waitForBatchedUpdatesWithAct(); - }) - .then(() => { - User.subscribeToUserEvents(); - return waitForBatchedUpdates(); - }) - .then(async () => { - await act(async () => { - // Simulate setting an unread report and personal details - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - lastMessageText: 'Test', - participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, - lastActorAccountID: USER_B_ACCOUNT_ID, - type: CONST.REPORT.TYPE.CHAT, - }); - - await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - - // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(); - }); - - await waitForBatchedUpdatesWithAct(); + await waitForBatchedUpdatesWithAct(); + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + await act(async () => { + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }); + + await waitForBatchedUpdatesWithAct(); + + User.subscribeToUserEvents(); + + await waitForBatchedUpdates(); + + await act(async () => { + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastMessageText: 'Test', + participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }); + + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + }); + + await waitForBatchedUpdatesWithAct(); } describe('Pagination', () => { @@ -262,36 +181,45 @@ describe('Pagination', () => { jest.clearAllMocks(); }); - it.only('opens a chat and load initial messages', async () => { - const TEN_MINUTES_AGO = subMinutes(new Date(), 10); - fetchMock.mockAPICommand('OpenReport', [ - { - onyxMethod: 'merge', - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - value: { - // '1': { - // reportActionID: '1', - // actionName: 'CREATED', - // created: format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING), - // }, - '2': TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2'), - '3': TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '3'), - }, - }, - ]); + it('opens a chat and load initial messages', async () => { + mockOpenReport(5, true); + await signInAndGetApp(); await navigateToSidebarOption(0); - const messageHintText = Localize.translateLocal('accessibilityHints.chatMessage'); - const messages = screen.queryAllByLabelText(messageHintText); + expect(getReportActions()).toHaveLength(5); + expectAPICommandToHaveBeenCalled('OpenReport', 1); + expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + + // Scrolling here should not trigger a new network request. + scrollToOffset(LIST_CONTENT_SIZE.height); + await waitForBatchedUpdatesWithAct(); + scrollToOffset(0); + await waitForBatchedUpdatesWithAct(); + + expectAPICommandToHaveBeenCalled('OpenReport', 1); + expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + }); + + it('opens a chat and load older messages', async () => { + mockOpenReport(5, false); - expect(fetchMock.mock.calls.filter((c) => c[0] === 'https://www.expensify.com.dev/api/OpenReport?')).toHaveLength(1); - expect(messages).toHaveLength(2); + await signInAndGetApp(); + await navigateToSidebarOption(0); - // Scrolling up here should not trigger a new network request. - const fetchCalls = fetchMock.mock.calls.length; - scrollToOffset(300); + expect(getReportActions()).toHaveLength(5); + expectAPICommandToHaveBeenCalled('OpenReport', 1); + expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + + // Scrolling here should trigger a new network request. + scrollToOffset(LIST_CONTENT_SIZE.height); await waitForBatchedUpdatesWithAct(); - expect(fetchMock.mock.calls.length).toBe(fetchCalls); + + expectAPICommandToHaveBeenCalled('OpenReport', 1); + expectAPICommandToHaveBeenCalled('GetOlderActions', 1); + expectAPICommandToHaveBeenCalled('GetNewerActions', 0); }); }); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 41bc2ba913c1..647eb4b0bc1f 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -1,30 +1,25 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type * as NativeNavigation from '@react-navigation/native'; +import * as NativeNavigation from '@react-navigation/native'; import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; import {addSeconds, format, subMinutes, subSeconds} from 'date-fns'; import {utcToZonedTime} from 'date-fns-tz'; import React from 'react'; -import {AppState, DeviceEventEmitter, Linking} from 'react-native'; +import {AppState, DeviceEventEmitter} from 'react-native'; import type {ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type Animated from 'react-native-reanimated'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import LocalNotification from '@libs/Notification/LocalNotification'; import * as NumberUtils from '@libs/NumberUtils'; -import * as Pusher from '@libs/Pusher/pusher'; -import PusherConnectionManager from '@libs/PusherConnectionManager'; import FontUtils from '@styles/utils/FontUtils'; import * as AppActions from '@userActions/App'; import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; import App from '@src/App'; -import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import appSetup from '@src/setup'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -34,105 +29,18 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(30000); +jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); jest.mock('../../src/components/ConfirmedRoute.tsx'); -// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest -jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ - __esModule: true, - default: { - ignoreLogs: jest.fn(), - ignoreAllLogs: jest.fn(), - }, -})); - -jest.mock('react-native-reanimated', () => ({ - ...jest.requireActual('react-native-reanimated/mock'), - createAnimatedPropAdapter: jest.fn, - useReducedMotion: jest.fn, -})); - -/** - * We need to keep track of the transitionEnd callback so we can trigger it in our tests - */ -let transitionEndCB: () => void; - -type ListenerMock = { - triggerTransitionEnd: () => void; - addListener: jest.Mock; -}; - -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. - * - * @returns An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = (): ListenerMock => { - const transitionEndListeners: Array<() => void> = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback: () => void) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - const {triggerTransitionEnd, addListener} = createAddListenerMock(); - transitionEndCB = triggerTransitionEnd; - - const useNavigation = () => - ({ - navigate: jest.fn(), - ...actualNav.useNavigation, - getState: () => ({ - routes: [], - }), - addListener, - } as typeof NativeNavigation.useNavigation); - - return { - ...actualNav, - useNavigation, - getState: () => ({ - routes: [], - }), - } as typeof NativeNavigation; -}); - -beforeAll(() => { - // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it - // return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means - // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling - // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to - // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. - global.fetch = TestHelper.getGlobalFetchMock(); - - Linking.setInitialURL('https://new.expensify.com/'); - appSetup(); - - // Connect to Pusher - PusherConnectionManager.init(); - Pusher.init({ - appKey: CONFIG.PUSHER.APP_KEY, - cluster: CONFIG.PUSHER.CLUSTER, - authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, - }); -}); +TestHelper.setupApp(); +// In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it +// return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means +// fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling +// behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to +// simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. +TestHelper.setupGlobalFetchMock(); function scrollUpToRevealNewMessagesBadge() { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); @@ -308,7 +216,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(async () => { - await act(() => transitionEndCB?.()); + await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); // That the report actions are visible along with the created action const welcomeMessageHintText = Localize.translateLocal('accessibilityHints.chatWelcomeMessage'); @@ -333,7 +241,7 @@ describe('Unread Indicators', () => { // Navigate to the unread chat from the sidebar .then(() => navigateToSidebarOption(0)) .then(async () => { - await act(() => transitionEndCB?.()); + await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); // Verify the unread indicator is present const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); @@ -453,7 +361,7 @@ describe('Unread Indicators', () => { }) .then(waitForBatchedUpdates) .then(async () => { - await act(() => transitionEndCB?.()); + await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText); @@ -530,7 +438,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(async () => { - await act(() => transitionEndCB?.()); + await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 102781cd07a2..f3aed289acff 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,10 +1,16 @@ +import type * as NativeNavigation from '@react-navigation/native'; import {Str} from 'expensify-common'; +import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; +import * as Pusher from '@libs/Pusher/pusher'; +import PusherConnectionManager from '@libs/PusherConnectionManager'; +import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import * as Session from '@src/libs/actions/Session'; import HttpUtils from '@src/libs/HttpUtils'; import * as NumberUtils from '@src/libs/NumberUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; @@ -26,7 +32,24 @@ type FormData = { entries: () => Array<[string, string | Blob]>; }; -type Listener = () => void; +type NativeNavigationMock = typeof NativeNavigation & { + triggerTransitionEnd: () => void; +}; + +function setupApp() { + beforeAll(() => { + Linking.setInitialURL('https://new.expensify.com/'); + appSetup(); + + // Connect to Pusher + PusherConnectionManager.init(); + Pusher.init({ + appKey: CONFIG.PUSHER.APP_KEY, + cluster: CONFIG.PUSHER.CLUSTER, + authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, + }); + }); +} function buildPersonalDetails(login: string, accountID: number, firstName = 'Test'): PersonalDetails { return { @@ -161,7 +184,7 @@ function signOutTestUser() { * - fail() - start returning a failure response * - success() - go back to returning a success response */ -function getGlobalFetchMock() { +function getGlobalFetchMock(): typeof fetch { let queue: QueueItem[] = []; let responses = new Map(); let isPaused = false; @@ -220,6 +243,19 @@ function getGlobalFetchMock() { return mockFetch as typeof fetch; } +function setupGlobalFetchMock(): MockFetch { + const mockFetch = getGlobalFetchMock(); + const originalFetch = global.fetch; + + global.fetch = mockFetch as unknown as typeof global.fetch; + + afterAll(() => { + global.fetch = originalFetch; + }); + + return mockFetch as MockFetch; +} + function setPersonalDetails(login: string, accountID: number) { Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [accountID]: buildPersonalDetails(login, accountID), @@ -248,30 +284,15 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * @returns An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = () => { - const transitionEndListeners: Listener[] = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; +export type {MockFetch, FormData, NativeNavigationMock}; +export { + assertFormDataMatchesObject, + buildPersonalDetails, + buildTestReportComment, + getGlobalFetchMock, + setupApp, + setupGlobalFetchMock, + setPersonalDetails, + signInWithTestUser, + signOutTestUser, }; - -export type {MockFetch, FormData}; -export {assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, createAddListenerMock, getGlobalFetchMock, setPersonalDetails, signInWithTestUser, signOutTestUser}; diff --git a/tests/utils/debug.ts b/tests/utils/debug.ts new file mode 100644 index 000000000000..ba27e9725b88 --- /dev/null +++ b/tests/utils/debug.ts @@ -0,0 +1,114 @@ +/** + * The debug utility that ships with react native testing library does not work properly and + * has limited functionality. This is a better version of it that allows logging a subtree of + * the app. + */ + +/* eslint-disable no-console, testing-library/no-node-access, testing-library/no-debugging-utils, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any */ +import type {NewPlugin} from 'pretty-format'; +import prettyFormat, {plugins} from 'pretty-format'; +import ReactIs from 'react-is'; +import type {ReactTestInstance, ReactTestRendererJSON} from 'react-test-renderer'; + +// These are giant objects and cause the serializer to crash because the +// output becomes too large. +const NativeComponentPlugin: NewPlugin = { + // eslint-disable-next-line no-underscore-dangle + test: (val) => !!val?._reactInternalInstance, + serialize: () => 'NativeComponentInstance {}', +}; + +type Options = { + includeProps?: boolean; + maxDepth?: number; +}; + +const format = (input: ReactTestRendererJSON | ReactTestRendererJSON[], options: Options) => + prettyFormat(input, { + plugins: [plugins.ReactTestComponent, plugins.ReactElement, NativeComponentPlugin], + highlight: true, + printBasicPrototype: false, + maxDepth: options.maxDepth, + }); + +function getType(element: any) { + const type = element.type; + if (typeof type === 'string') { + return type; + } + if (typeof type === 'function') { + return type.displayName || type.name || 'Unknown'; + } + + if (ReactIs.isFragment(element)) { + return 'React.Fragment'; + } + if (ReactIs.isSuspense(element)) { + return 'React.Suspense'; + } + if (typeof type === 'object' && type !== null) { + if (ReactIs.isContextProvider(element)) { + return 'Context.Provider'; + } + + if (ReactIs.isContextConsumer(element)) { + return 'Context.Consumer'; + } + + if (ReactIs.isForwardRef(element)) { + if (type.displayName) { + return type.displayName; + } + + const functionName = type.render.displayName || type.render.name || ''; + + return functionName === '' ? 'ForwardRef' : `ForwardRef(${functionName})`; + } + + if (ReactIs.isMemo(element)) { + const functionName = type.displayName || type.type.displayName || type.type.name || ''; + + return functionName === '' ? 'Memo' : `Memo(${functionName})`; + } + } + return 'UNDEFINED'; +} + +function getProps(props: Record, options: Options) { + if (!options.includeProps) { + return {}; + } + const {children, ...propsWithoutChildren} = props; + return propsWithoutChildren; +} + +function toJSON(node: ReactTestInstance, options: Options): ReactTestRendererJSON { + const json = { + $$typeof: Symbol.for('react.test.json'), + type: getType({type: node.type, $$typeof: Symbol.for('react.element')}), + props: getProps(node.props, options), + children: node.children?.map((c) => (typeof c === 'string' ? c : toJSON(c, options))) ?? null, + }; + + return json; +} + +function formatNode(node: ReactTestInstance, options: Options) { + return format(toJSON(node, options), options); +} + +/** + * Log a subtree of the app for debugging purposes. + * + * @example debug(screen.getByTestId('report-actions-view-container')); + */ +export default function debug(node: ReactTestInstance | ReactTestInstance[] | null, {includeProps = true, maxDepth = Infinity}: Options = {}): void { + const options = {includeProps, maxDepth}; + if (node == null) { + console.log('null'); + } else if (Array.isArray(node)) { + console.log(node.map((n) => formatNode(n, options)).join('\n')); + } else { + console.log(formatNode(node, options)); + } +} From fa845ca88854c11dc4773970c99f31270a068f19 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 14 Jun 2024 18:09:19 -0400 Subject: [PATCH 096/512] Fix findLastItem --- src/libs/PaginationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 75725776a6c9..5ac438925f7d 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -45,7 +45,7 @@ function findFirstItem(sortedItems: TResource[], page: string[], getI * Finds the id and index in sortedItems of the last item in the given page that's present in sortedItems. */ function findLastItem(sortedItems: TResource[], page: string[], getID: (item: TResource) => string): ItemWithIndex | null { - for (let i = page.length - 1; i > 0; i--) { + for (let i = page.length - 1; i >= 0; i--) { const id = page[i]; if (id === CONST.PAGINATION_END_ID) { return {id, index: sortedItems.length - 1}; From 879efb76df8de3316fed4489b73259945551ae98 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 11:54:05 +0800 Subject: [PATCH 097/512] update translation --- src/components/MoneyReportHeader.tsx | 1 + src/components/ProcessMoneyReportHoldMenu.tsx | 17 ++++++++++------- .../ReportActionItem/ReportPreview.tsx | 1 + src/languages/en.ts | 5 +++-- src/languages/types.ts | 3 +++ 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 90952157f179..38ef7812c117 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -356,6 +356,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea paymentType={paymentType} chatReport={chatReport} moneyRequestReport={moneyRequestReport} + transactionCount={transactionIDs.length} /> )} { - let promptTranslation: TranslationPaths; if (nonHeldAmount) { - promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; + return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); } else { - promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount'; + return translate( + isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', + {transactionCount} + ); } - - return translate(promptTranslation); - }, [nonHeldAmount]); + }, [nonHeldAmount, transactionCount, translate, isApprove]); return ( )} diff --git a/src/languages/en.ts b/src/languages/en.ts index 59c9da46a36d..3b2ac5c46d8d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -13,6 +13,7 @@ import type { BeginningOfChatHistoryDomainRoomPartOneParams, CanceledRequestParams, CharacterLimitParams, + ConfirmHoldExpenseParams, ConfirmThatParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, @@ -766,10 +767,10 @@ export default { reviewDuplicates: 'Review duplicates', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", - confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', + confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", - confirmPayAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and pay?', + confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', diff --git a/src/languages/types.ts b/src/languages/types.ts index de9b1d2dadeb..b6553be196e1 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -298,6 +298,8 @@ type DistanceRateOperationsParams = {count: number}; type ReimbursementRateParams = {unit: Unit}; +type ConfirmHoldExpenseParams = {transactionCount: number}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -309,6 +311,7 @@ export type { BeginningOfChatHistoryDomainRoomPartOneParams, CanceledRequestParams, CharacterLimitParams, + ConfirmHoldExpenseParams, ConfirmThatParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, From fc6515ed4fe59e189333d85b00f31fd249928c87 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 11:56:02 +0800 Subject: [PATCH 098/512] prettier --- src/components/ProcessMoneyReportHoldMenu.tsx | 5 +---- src/languages/en.ts | 6 ++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 8986083ee3c6..9f3839fe2a13 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -76,10 +76,7 @@ function ProcessMoneyReportHoldMenu({ if (nonHeldAmount) { return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); } else { - return translate( - isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', - {transactionCount} - ); + return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); } }, [nonHeldAmount, transactionCount, translate, isApprove]); diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b2ac5c46d8d..0a47ac58bcbd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -767,10 +767,12 @@ export default { reviewDuplicates: 'Review duplicates', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", - confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, + confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => + `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", - confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, + confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => + `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', From 0cc82d7849b171ed04d5126aec2f2ee520a2c588 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 12:25:33 +0800 Subject: [PATCH 099/512] lint --- src/components/ProcessMoneyReportHoldMenu.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 9f3839fe2a13..872464d8a5b0 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -75,9 +75,8 @@ function ProcessMoneyReportHoldMenu({ const promptText = useMemo(() => { if (nonHeldAmount) { return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); - } else { - return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); } + return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); }, [nonHeldAmount, transactionCount, translate, isApprove]); return ( From 4fa559f121748d35ac88aae7646ac7f72166b57c Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 15 Jun 2024 08:50:31 +0200 Subject: [PATCH 100/512] Fix bug with animation for carousel x2 --- src/components/MultiGestureCanvas/usePanGesture.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index c236393027ef..08d0d94d64d6 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -3,6 +3,7 @@ import {Dimensions} from 'react-native'; import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import * as Browser from '@libs/Browser'; import {SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -206,6 +207,10 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; + if (!isSwipingDownToClose.value && !Browser.isMobile()) { + panTranslateX.value += evt.changeX; + } + if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { panTranslateY.value += evt.changeY; } From d133a3dee507affa61811186f9303a9350baa418 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 18 Jun 2024 12:25:12 +0800 Subject: [PATCH 101/512] update copy --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b24c8b244b4d..293df4aa294c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -770,7 +770,7 @@ export default { confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', - confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", + confirmPayAmount: "Pay what's not on hold, or pay the entire report.", confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', From 3005ecca952e7edf234ba251592c9706be7c3a07 Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 18 Jun 2024 11:32:39 +0200 Subject: [PATCH 102/512] masking fragile data added --- src/libs/ExportOnyxState/index.native.ts | 7 +++-- src/libs/ExportOnyxState/index.ts | 29 +++++++++++++++++-- .../Troubleshoot/TroubleshootPage.tsx | 8 ++--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index 2d53b9c39b5f..8b2145900949 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,6 +1,7 @@ import {open} from 'react-native-quick-sqlite'; import RNFS from "react-native-fs"; import Share from "react-native-share"; +import * as main from './index'; const readFromIndexedDB = () => new Promise((resolve) => { const db = open({name: 'OnyxDB'}); @@ -16,8 +17,7 @@ const readFromIndexedDB = () => new Promise((resolve) => { }); -// eslint-disable-next-line @lwc/lwc/no-async-await -const shareAsFile = async (value: string) => { +const shareAsFile = (value: string) => { try { // Define new filename and path for the app info file const infoFileName = `onyx-state.txt`; @@ -30,13 +30,14 @@ const shareAsFile = async (value: string) => { urls: [actualInfoFile], }; - await Share.open(shareOptions); + Share.open(shareOptions); } catch (error) { console.error('Error renaming and sharing file:', error); } } export default { + maskFragileData: main.default.maskFragileData, readFromIndexedDB, shareAsFile, } diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index e7427b097ae2..1d610812d40d 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,4 +1,6 @@ -const readFromIndexedDB = () => new Promise((resolve) => { +import {Str} from "expensify-common"; + +const readFromIndexedDB = () => new Promise>((resolve) => { let db: IDBDatabase; const openRequest = indexedDB.open('OnyxDB', 1); openRequest.onsuccess = () => { @@ -26,8 +28,28 @@ const readFromIndexedDB = () => new Promise((resolve) => { }; }); -// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await -const shareAsFile = async (value: string) => { +const maskFragileData = (data: Record): Record => { + const maskedData: Record = {}; + + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const value = data[key]; + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } + else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record); + } + else { + maskedData[key] = value; + } + } + } + + return maskedData; +} + +const shareAsFile = (value: string) => { const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(value)}`); element.setAttribute('download', 'onyx-state.txt'); @@ -41,6 +63,7 @@ const shareAsFile = async (value: string) => { } export default { + maskFragileData, readFromIndexedDB, shareAsFile, } diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 8b9e174697e5..a87ffbcb8980 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -55,12 +55,10 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const illustrationStyle = getLightbulbIllustrationStyle(); const exportOnyxState = () => { - ExportOnyxState.readFromIndexedDB().then((value) => { - console.log('exported indexedDB state: ', value); + ExportOnyxState.readFromIndexedDB().then((value: Record) => { + const maskedData = ExportOnyxState.maskFragileData(value); - ExportOnyxState.shareAsFile(JSON.stringify(value)).then(() => { - console.log('exported indexedDB state as file'); - }); + ExportOnyxState.shareAsFile(JSON.stringify(maskedData)); }); }; From ed03efe2f73e0151c532c2525c659638cefa32cb Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 18 Jun 2024 11:52:07 +0200 Subject: [PATCH 103/512] prettier --- src/libs/ExportOnyxState/index.ts | 64 +++++++++---------- .../Troubleshoot/TroubleshootPage.tsx | 6 +- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 1d610812d40d..5c1817281934 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,32 +1,32 @@ -import {Str} from "expensify-common"; +import {Str} from 'expensify-common'; -const readFromIndexedDB = () => new Promise>((resolve) => { - let db: IDBDatabase; - const openRequest = indexedDB.open('OnyxDB', 1); - openRequest.onsuccess = () => { - db = openRequest.result; - const transaction = db.transaction('keyvaluepairs'); - const objectStore = transaction.objectStore('keyvaluepairs'); - const cursor = objectStore.openCursor(); +const readFromIndexedDB = () => + new Promise>((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open('OnyxDB', 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction('keyvaluepairs'); + const objectStore = transaction.objectStore('keyvaluepairs'); + const cursor = objectStore.openCursor(); - const queryResult: Record = {}; + const queryResult: Record = {}; - cursor.onerror = () => { - console.error('Error reading cursor'); - } + cursor.onerror = () => { + console.error('Error reading cursor'); + }; - cursor.onsuccess = (event) => { - const { result } = event.target as IDBRequest; - if (result) { - queryResult[result.primaryKey as string] = result.value; - result.continue(); - } - else { - resolve(queryResult); - } - } - }; -}); + cursor.onsuccess = (event) => { + const {result} = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } else { + resolve(queryResult); + } + }; + }; + }); const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; @@ -36,22 +36,20 @@ const maskFragileData = (data: Record): Record const value = data[key]; if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { maskedData[key] = '***'; - } - else if (typeof value === 'object') { + } else if (typeof value === 'object') { maskedData[key] = maskFragileData(value as Record); - } - else { + } else { maskedData[key] = value; } } } return maskedData; -} +}; const shareAsFile = (value: string) => { const element = document.createElement('a'); - element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(value)}`); + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); element.setAttribute('download', 'onyx-state.txt'); element.style.display = 'none'; @@ -60,10 +58,10 @@ const shareAsFile = (value: string) => { element.click(); document.body.removeChild(element); -} +}; export default { maskFragileData, readFromIndexedDB, shareAsFile, -} +}; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index a87ffbcb8980..ce6586d8f275 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -22,6 +22,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import ExportOnyxState from '@libs/ExportOnyxState'; import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; @@ -29,7 +30,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import ExportOnyxState from '@libs/ExportOnyxState'; import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; type BaseMenuItem = { @@ -78,8 +78,8 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { { translationKey: 'initialSettingsPage.troubleshoot.exportOnyxState', icon: Expensicons.Download, - action: exportOnyxState - } + action: exportOnyxState, + }, ]; if (shouldStoreLogs) { From 4160cc878a62a3b7bc49176fc252d459e25f3ab9 Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:28:11 +0200 Subject: [PATCH 104/512] common part extracted --- src/libs/ExportOnyxState/common.ts | 24 +++++++++++++ src/libs/ExportOnyxState/index.native.ts | 36 +++++++++---------- src/libs/ExportOnyxState/index.ts | 30 ++++------------ .../Troubleshoot/TroubleshootPage.tsx | 2 +- 4 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 src/libs/ExportOnyxState/common.ts diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts new file mode 100644 index 000000000000..09b2d955400e --- /dev/null +++ b/src/libs/ExportOnyxState/common.ts @@ -0,0 +1,24 @@ +import {Str} from 'expensify-common'; + +const maskFragileData = (data: Record): Record => { + const maskedData: Record = {}; + + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const value = data[key]; + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record); + } else { + maskedData[key] = value; + } + } + } + + return maskedData; +}; + +export default { + maskFragileData, +}; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index 8b2145900949..de9c34153c5c 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,23 +1,23 @@ +import RNFS from 'react-native-fs'; import {open} from 'react-native-quick-sqlite'; -import RNFS from "react-native-fs"; -import Share from "react-native-share"; -import * as main from './index'; +import Share from 'react-native-share'; +import common from './common'; -const readFromIndexedDB = () => new Promise((resolve) => { - const db = open({name: 'OnyxDB'}); - const query = 'SELECT * FROM keyvaluepairs'; +const readFromOnyxDatabase = () => + new Promise((resolve) => { + const db = open({name: 'OnyxDB'}); + const query = 'SELECT * FROM keyvaluepairs'; - db.executeAsync(query, []).then(({rows}) => { - // eslint-disable-next-line no-underscore-dangle - const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); + db.executeAsync(query, []).then(({rows}) => { + // eslint-disable-next-line no-underscore-dangle + const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); - resolve(result); - db.close(); + resolve(result); + }); }); -}); - -const shareAsFile = (value: string) => { +// eslint-disable-next-line @lwc/lwc/no-async-await +const shareAsFile = async (value: string) => { try { // Define new filename and path for the app info file const infoFileName = `onyx-state.txt`; @@ -34,10 +34,10 @@ const shareAsFile = (value: string) => { } catch (error) { console.error('Error renaming and sharing file:', error); } -} +}; export default { - maskFragileData: main.default.maskFragileData, - readFromIndexedDB, + maskFragileData: common.maskFragileData, + readFromOnyxDatabase, shareAsFile, -} +}; diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 5c1817281934..840fc26eeb20 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,6 +1,6 @@ -import {Str} from 'expensify-common'; +import common from './common'; -const readFromIndexedDB = () => +const readFromOnyxDatabase = () => new Promise>((resolve) => { let db: IDBDatabase; const openRequest = indexedDB.open('OnyxDB', 1); @@ -28,26 +28,8 @@ const readFromIndexedDB = () => }; }); -const maskFragileData = (data: Record): Record => { - const maskedData: Record = {}; - - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const value = data[key]; - if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { - maskedData[key] = '***'; - } else if (typeof value === 'object') { - maskedData[key] = maskFragileData(value as Record); - } else { - maskedData[key] = value; - } - } - } - - return maskedData; -}; - -const shareAsFile = (value: string) => { +// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await +const shareAsFile = async (value: string) => { const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); element.setAttribute('download', 'onyx-state.txt'); @@ -61,7 +43,7 @@ const shareAsFile = (value: string) => { }; export default { - maskFragileData, - readFromIndexedDB, + maskFragileData: common.maskFragileData, + readFromOnyxDatabase, shareAsFile, }; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index ce6586d8f275..bfe512df8526 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -55,7 +55,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const illustrationStyle = getLightbulbIllustrationStyle(); const exportOnyxState = () => { - ExportOnyxState.readFromIndexedDB().then((value: Record) => { + ExportOnyxState.readFromOnyxDatabase().then((value: Record) => { const maskedData = ExportOnyxState.maskFragileData(value); ExportOnyxState.shareAsFile(JSON.stringify(maskedData)); From 934346de737ddca157970adfc4e441e34d33ca6b Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:35:40 +0200 Subject: [PATCH 105/512] useCallback usage added --- src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index bfe512df8526..0a763ad75562 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import Onyx, {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -54,13 +54,13 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); - const exportOnyxState = () => { + const exportOnyxState = useCallback(() => { ExportOnyxState.readFromOnyxDatabase().then((value: Record) => { const maskedData = ExportOnyxState.maskFragileData(value); ExportOnyxState.shareAsFile(JSON.stringify(maskedData)); }); - }; + }, []); const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { From 6c0574db7f10eb54a7f3ee3f725b280ea0374d2a Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:48:17 +0200 Subject: [PATCH 106/512] const values extracted --- src/CONST.ts | 3 +++ src/libs/ExportOnyxState/common.ts | 5 +++++ src/libs/ExportOnyxState/index.native.ts | 22 +++++++++++----------- src/libs/ExportOnyxState/index.ts | 12 ++++++------ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3d67a951111e..4cdcb61da6d4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -74,6 +74,9 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + DEFAULT_DB_NAME: 'OnyxDB', + DEFAULT_TABLE_NAME: 'keyvaluepairs', + DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index 09b2d955400e..b85daa27615d 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -1,5 +1,8 @@ import {Str} from 'expensify-common'; +const ONYX_DB_KEY = 'OnyxDB'; +const DEFAULT_FILE_NAME = 'onyx-state.txt'; + const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; @@ -21,4 +24,6 @@ const maskFragileData = (data: Record): Record export default { maskFragileData, + ONYX_DB_KEY, + DEFAULT_FILE_NAME, }; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index de9c34153c5c..ff8ff9e4f730 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,12 +1,13 @@ import RNFS from 'react-native-fs'; import {open} from 'react-native-quick-sqlite'; import Share from 'react-native-share'; +import CONST from '@src/CONST'; import common from './common'; const readFromOnyxDatabase = () => new Promise((resolve) => { - const db = open({name: 'OnyxDB'}); - const query = 'SELECT * FROM keyvaluepairs'; + const db = open({name: CONST.DEFAULT_DB_NAME}); + const query = `SELECT * FROM ${CONST.DEFAULT_TABLE_NAME}`; db.executeAsync(query, []).then(({rows}) => { // eslint-disable-next-line no-underscore-dangle @@ -16,21 +17,20 @@ const readFromOnyxDatabase = () => }); }); -// eslint-disable-next-line @lwc/lwc/no-async-await -const shareAsFile = async (value: string) => { +const shareAsFile = (value: string) => { try { // Define new filename and path for the app info file - const infoFileName = `onyx-state.txt`; + const infoFileName = CONST.DEFAULT_ONYX_DUMP_FILE_NAME; const infoFilePath = `${RNFS.DocumentDirectoryPath}/${infoFileName}`; const actualInfoFile = `file://${infoFilePath}`; - await RNFS.writeFile(infoFilePath, value, 'utf8'); + RNFS.writeFile(infoFilePath, value, 'utf8').then(() => { + const shareOptions = { + urls: [actualInfoFile], + }; - const shareOptions = { - urls: [actualInfoFile], - }; - - Share.open(shareOptions); + Share.open(shareOptions); + }); } catch (error) { console.error('Error renaming and sharing file:', error); } diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 840fc26eeb20..b47b3a01fb8b 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,13 +1,14 @@ +import CONST from '@src/CONST'; import common from './common'; const readFromOnyxDatabase = () => new Promise>((resolve) => { let db: IDBDatabase; - const openRequest = indexedDB.open('OnyxDB', 1); + const openRequest = indexedDB.open(CONST.DEFAULT_DB_NAME, 1); openRequest.onsuccess = () => { db = openRequest.result; - const transaction = db.transaction('keyvaluepairs'); - const objectStore = transaction.objectStore('keyvaluepairs'); + const transaction = db.transaction(CONST.DEFAULT_TABLE_NAME); + const objectStore = transaction.objectStore(CONST.DEFAULT_TABLE_NAME); const cursor = objectStore.openCursor(); const queryResult: Record = {}; @@ -28,11 +29,10 @@ const readFromOnyxDatabase = () => }; }); -// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await -const shareAsFile = async (value: string) => { +const shareAsFile = (value: string) => { const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); - element.setAttribute('download', 'onyx-state.txt'); + element.setAttribute('download', CONST.DEFAULT_ONYX_DUMP_FILE_NAME); element.style.display = 'none'; document.body.appendChild(element); From 56fe0cda27afac0f8ee3a5033e8f8ebfffead9f4 Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:50:49 +0200 Subject: [PATCH 107/512] unnecessary variables removed --- src/libs/ExportOnyxState/common.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index b85daa27615d..09b2d955400e 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -1,8 +1,5 @@ import {Str} from 'expensify-common'; -const ONYX_DB_KEY = 'OnyxDB'; -const DEFAULT_FILE_NAME = 'onyx-state.txt'; - const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; @@ -24,6 +21,4 @@ const maskFragileData = (data: Record): Record export default { maskFragileData, - ONYX_DB_KEY, - DEFAULT_FILE_NAME, }; From 8423971c1ec3e4555ee97c75d4487efc67ebeea2 Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:51:01 +0200 Subject: [PATCH 108/512] useMemo deps array updated --- src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 0a763ad75562..1253c96b8758 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -95,7 +95,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { wrapperStyle: [styles.sectionMenuItemTopDescription], })) .reverse(); - }, [shouldStoreLogs, translate, waitForNavigate, styles.sectionMenuItemTopDescription]); + }, [waitForNavigate, exportOnyxState, shouldStoreLogs, translate, styles.sectionMenuItemTopDescription]); return ( Date: Wed, 19 Jun 2024 10:54:00 +0200 Subject: [PATCH 109/512] additional comment added --- src/libs/ExportOnyxState/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index b47b3a01fb8b..148548ce5d1c 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -23,6 +23,7 @@ const readFromOnyxDatabase = () => queryResult[result.primaryKey as string] = result.value; result.continue(); } else { + // no results mean the cursor has reached the end of the data resolve(queryResult); } }; From d023cf52b5fa1d4b9a4afedbf95f82ee53f4dd08 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 19 Jun 2024 18:44:07 +0800 Subject: [PATCH 110/512] remove autoFocus prop --- src/pages/signin/LoginForm/BaseLoginForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index 1c8084eb12e2..49e7479c0435 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -267,7 +267,6 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false onSubmitEditing={validateAndSubmitForm} autoCapitalize="none" autoCorrect={false} - autoFocus inputMode={CONST.INPUT_MODE.EMAIL} errorText={formError} hasError={shouldShowServerError} From aea756f9a9c3de08fca8b91268fe6755eb179554 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 19 Jun 2024 18:44:21 +0800 Subject: [PATCH 111/512] don't set initial focus for sign in modal --- src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts index 2a77b52e3116..ec2977ea592c 100644 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -10,6 +10,7 @@ const SCREENS_WITH_AUTOFOCUS: string[] = [ SCREENS.SETTINGS.PROFILE.PRONOUNS, SCREENS.NEW_TASK.DETAILS, SCREENS.MONEY_REQUEST.CREATE, + SCREENS.SIGN_IN_ROOT, ]; export default SCREENS_WITH_AUTOFOCUS; From 2f676e87215f51725fbf6d9c6f276513617c40b3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:41:39 +0200 Subject: [PATCH 112/512] Pass generic parameter to all lottie requires --- src/components/LottieAnimations/index.tsx | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index 657fe79b401f..63e31a60a95b 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -1,79 +1,80 @@ +import type {LottieViewProps} from 'lottie-react-native'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import type DotLottieAnimation from './types'; const DotLottieAnimations = { Abracadabra: { - file: require('@assets/animations/Abracadabra.lottie'), + file: require('@assets/animations/Abracadabra.lottie'), w: 375, h: 400, }, FastMoney: { - file: require('@assets/animations/FastMoney.lottie'), + file: require('@assets/animations/FastMoney.lottie'), w: 375, h: 240, }, Fireworks: { - file: require('@assets/animations/Fireworks.lottie'), + file: require('@assets/animations/Fireworks.lottie'), w: 360, h: 360, }, Hands: { - file: require('@assets/animations/Hands.lottie'), + file: require('@assets/animations/Hands.lottie'), w: 375, h: 375, }, PreferencesDJ: { - file: require('@assets/animations/PreferencesDJ.lottie'), + file: require('@assets/animations/PreferencesDJ.lottie'), w: 375, h: 240, backgroundColor: colors.blue500, }, ReviewingBankInfo: { - file: require('@assets/animations/ReviewingBankInfo.lottie'), + file: require('@assets/animations/ReviewingBankInfo.lottie'), w: 280, h: 280, }, WorkspacePlanet: { - file: require('@assets/animations/WorkspacePlanet.lottie'), + file: require('@assets/animations/WorkspacePlanet.lottie'), w: 375, h: 240, backgroundColor: colors.pink800, }, SaveTheWorld: { - file: require('@assets/animations/SaveTheWorld.lottie'), + file: require('@assets/animations/SaveTheWorld.lottie'), w: 375, h: 240, }, Safe: { - file: require('@assets/animations/Safe.lottie'), + file: require('@assets/animations/Safe.lottie'), w: 625, h: 400, backgroundColor: colors.ice500, }, Magician: { - file: require('@assets/animations/Magician.lottie'), + file: require('@assets/animations/Magician.lottie'), w: 853, h: 480, }, Update: { - file: require('@assets/animations/Update.lottie'), + file: require('@assets/animations/Update.lottie'), w: variables.updateAnimationW, h: variables.updateAnimationH, }, Coin: { - file: require('@assets/animations/Coin.lottie'), + file: require('@assets/animations/Coin.lottie'), w: 375, h: 240, backgroundColor: colors.yellow600, }, Desk: { - file: require('@assets/animations/Desk.lottie'), + file: require('@assets/animations/Desk.lottie'), w: 200, h: 120, }, Plane: { - file: require('@assets/animations/Plane.lottie'), + file: require('@assets/animations/Plane.lottie'), w: 180, h: 200, }, From c2cbd8efacad81c387151b99cac0cc6b260ba80a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:41:56 +0200 Subject: [PATCH 113/512] Remove unsafe assigment --- .eslintrc.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f909258cb0ee..4a3291c86023 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -105,8 +105,6 @@ module.exports = { __DEV__: 'readonly', }, rules: { - '@typescript-eslint/no-unsafe-assignment': 'off', - // TypeScript specific rules '@typescript-eslint/prefer-enum-initializers': 'error', '@typescript-eslint/no-var-requires': 'off', From dd47f88ba81e19840d3af734a2587d17ed11a078 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:42:32 +0200 Subject: [PATCH 114/512] Add asserts to JSON.parse --- .github/actions/javascript/bumpVersion/bumpVersion.ts | 2 +- .../createOrUpdateStagingDeploy.ts | 4 ++-- .../actions/javascript/getGraphiteString/getGraphiteString.ts | 2 +- .../javascript/getPreviousVersion/getPreviousVersion.ts | 2 +- .../validateReassureOutput/validateReassureOutput.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/javascript/bumpVersion/bumpVersion.ts b/.github/actions/javascript/bumpVersion/bumpVersion.ts index eba79c7c9edb..6c0aab1bdb43 100644 --- a/.github/actions/javascript/bumpVersion/bumpVersion.ts +++ b/.github/actions/javascript/bumpVersion/bumpVersion.ts @@ -48,7 +48,7 @@ if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } -const {version: previousVersion}: PackageJson = JSON.parse(fs.readFileSync('./package.json').toString()); +const {version: previousVersion} = JSON.parse(fs.readFileSync('./package.json').toString()) as PackageJson; if (!previousVersion) { core.setFailed('Error: Could not read package.json'); } diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts index aed8b9dcba0a..caff455e9fa5 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts @@ -8,13 +8,13 @@ import GitUtils from '@github/libs/GitUtils'; type IssuesCreateResponse = Awaited>['data']; -type PackageJSON = { +type PackageJson = { version: string; }; async function run(): Promise { // Note: require('package.json').version does not work because ncc will resolve that to a plain string at compile time - const packageJson: PackageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) as PackageJson; const newVersionTag = packageJson.version; try { diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index 5231caa79ed5..93d5d8a9618b 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -33,7 +33,7 @@ const run = () => { } try { - const current: RegressionEntry = JSON.parse(entry); + const current = JSON.parse(entry) as RegressionEntry; // Extract timestamp, Graphite accepts timestamp in seconds if (current.metadata?.creationDate) { diff --git a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts index 262b603124fa..5e8839b72444 100644 --- a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts +++ b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts @@ -8,7 +8,7 @@ if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVEL core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); } -const {version: currentVersion}: PackageJson = JSON.parse(readFileSync('./package.json', 'utf8')); +const {version: currentVersion} = JSON.parse(readFileSync('./package.json', 'utf8')) as PackageJson; if (!currentVersion) { core.setFailed('Error: Could not read package.json'); } diff --git a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts index ad0f393a96a2..d843caf61518 100644 --- a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts +++ b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts @@ -3,7 +3,7 @@ import type {CompareResult, PerformanceEntry} from '@callstack/reassure-compare/ import fs from 'fs'; const run = (): boolean => { - const regressionOutput: CompareResult = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); + const regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')) as CompareResult; const countDeviation = Number(core.getInput('COUNT_DEVIATION', {required: true})); const durationDeviation = Number(core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true})); From 711d0282b2fb2fb7940c1568271dd7ff38c9a50b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:43:20 +0200 Subject: [PATCH 115/512] Use assertion for @vue/preload-webpack-plugin require --- config/webpack/webpack.common.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index bedd7e50ef94..33fd9131eca0 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -4,13 +4,13 @@ import dotenv from 'dotenv'; import fs from 'fs'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; -import type {Compiler, Configuration} from 'webpack'; +import type {Class} from 'type-fest'; +import type {Configuration, WebpackPluginInstance} from 'webpack'; import {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} from 'webpack'; import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import CustomVersionFilePlugin from './CustomVersionFilePlugin'; import type Environment from './types'; -// importing anything from @vue/preload-webpack-plugin causes an error type Options = { rel: string; as: string; @@ -18,13 +18,10 @@ type Options = { include: string; }; -type PreloadWebpackPluginClass = { - new (options?: Options): PreloadWebpackPluginClass; - apply: (compiler: Compiler) => void; -}; +type PreloadWebpackPluginClass = Class; -// require is necessary, there are no types for this package and the declaration file can't be seen by the build process which causes an error. -const PreloadWebpackPlugin: PreloadWebpackPluginClass = require('@vue/preload-webpack-plugin'); +// require is necessary, importing anything from @vue/preload-webpack-plugin causes an error +const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin') as PreloadWebpackPluginClass; const includeModules = [ 'react-native-animatable', From 13f8c0f5f71e5bb5a57e04d2bf32691472222fba Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:44:09 +0200 Subject: [PATCH 116/512] Fix some of jest scripts and mocks --- jest/setup.ts | 4 ++-- jest/setupMockFullstoryLib.ts | 2 +- src/libs/__mocks__/Permissions.ts | 3 ++- src/libs/actions/__mocks__/App.ts | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/jest/setup.ts b/jest/setup.ts index f11a8a4ed631..1485d743dba2 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -1,5 +1,6 @@ import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; +import type * as RNKeyboardController from 'react-native-keyboard-controller'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import 'setimmediate'; import mockFSLibrary from './setupMockFullstoryLib'; @@ -54,5 +55,4 @@ jest.mock('react-native-share', () => ({ default: jest.fn(), })); -// eslint-disable-next-line @typescript-eslint/no-unsafe-return -jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts index 9edfccab9441..eae3ea1f51bd 100644 --- a/jest/setupMockFullstoryLib.ts +++ b/jest/setupMockFullstoryLib.ts @@ -15,7 +15,7 @@ export default function mockFSLibrary() { return { FSPage(): FSPageInterface { return { - start: jest.fn(), + start: jest.fn(() => {}), }; }, default: Fullstory, diff --git a/src/libs/__mocks__/Permissions.ts b/src/libs/__mocks__/Permissions.ts index 634626a507af..702aec6a7bd4 100644 --- a/src/libs/__mocks__/Permissions.ts +++ b/src/libs/__mocks__/Permissions.ts @@ -1,3 +1,4 @@ +import type Permissions from '@libs/Permissions'; import CONST from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; @@ -9,7 +10,7 @@ import type Beta from '@src/types/onyx/Beta'; */ export default { - ...jest.requireActual('../Permissions'), + ...jest.requireActual('../Permissions'), canUseDefaultRooms: (betas: Beta[]) => betas.includes(CONST.BETAS.DEFAULT_ROOMS), canUseViolations: (betas: Beta[]) => betas.includes(CONST.BETAS.VIOLATIONS), }; diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 3d2b5814684b..4e812b31e3a3 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -5,7 +5,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import createProxyForObject from '@src/utils/createProxyForObject'; -const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); +const AppImplementation = jest.requireActual('@libs/actions/App'); const { setLocale, setLocaleAndNavigate, @@ -40,7 +40,7 @@ const mockValues: AppMockValues = { }; const mockValuesProxy = createProxyForObject(mockValues); -const ApplyUpdatesImplementation: typeof ApplyUpdatesImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); +const ApplyUpdatesImplementation = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); const getMissingOnyxUpdates = jest.fn((_fromID: number, toID: number) => { if (mockValuesProxy.missingOnyxUpdatesToBeApplied === undefined) { return Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, toID); From a7ab17344958e9eb0fcf93f0251562eea18daec3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:45:04 +0200 Subject: [PATCH 117/512] Add assertion to Console module --- src/libs/Console/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/Console/index.ts b/src/libs/Console/index.ts index f03d33674bde..9bbdb173e61b 100644 --- a/src/libs/Console/index.ts +++ b/src/libs/Console/index.ts @@ -87,8 +87,7 @@ const charMap: Record = { * @param text the text to sanitize * @returns the sanitized text */ -function sanitizeConsoleInput(text: string) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return +function sanitizeConsoleInput(text: string): string { return text.replace(charsToSanitize, (match) => charMap[match]); } @@ -102,7 +101,7 @@ function createLog(text: string) { try { // @ts-expect-error Any code inside `sanitizedInput` that gets evaluated by `eval()` will be executed in the context of the current this value. // eslint-disable-next-line no-eval, no-invalid-this - const result = eval.call(this, text); + const result = eval.call(this, text) as unknown; if (result !== undefined) { return [ @@ -131,7 +130,7 @@ function parseStringifiedMessages(logs: Log[]): Log[] { return logs.map((log) => { try { - const parsedMessage = JSON.parse(log.message); + const parsedMessage = JSON.parse(log.message) as Log['message']; return { ...log, message: parsedMessage, From 7fa04b473272729da598a4f29fe6f4e4cdbb9cc7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:45:40 +0200 Subject: [PATCH 118/512] Add assertions to yaml parser --- workflow_tests/utils/JobMocker.ts | 4 +--- workflow_tests/utils/preGenerateTest.ts | 2 +- workflow_tests/utils/utils.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/workflow_tests/utils/JobMocker.ts b/workflow_tests/utils/JobMocker.ts index 78be5c5861e6..9434b52ef0a5 100644 --- a/workflow_tests/utils/JobMocker.ts +++ b/workflow_tests/utils/JobMocker.ts @@ -94,9 +94,7 @@ class JobMocker { } readWorkflowFile(location: PathOrFileDescriptor): YamlWorkflow { - const test: YamlWorkflow = yaml.parse(fs.readFileSync(location, 'utf8')); - - return test; + return yaml.parse(fs.readFileSync(location, 'utf8')) as YamlWorkflow; } writeWorkflowFile(location: PathOrFileDescriptor, data: YamlWorkflow) { diff --git a/workflow_tests/utils/preGenerateTest.ts b/workflow_tests/utils/preGenerateTest.ts index 1fbbd6e3de4c..2c2e18eaeafa 100644 --- a/workflow_tests/utils/preGenerateTest.ts +++ b/workflow_tests/utils/preGenerateTest.ts @@ -275,7 +275,7 @@ checkIfMocksFileExists(workflowTestMocksDirectory, workflowTestMocksFileName); const workflowTestAssertionsFileName = `${workflowName}Assertions.ts`; checkIfAssertionsFileExists(workflowTestAssertionsDirectory, workflowTestAssertionsFileName); -const workflow: YamlWorkflow = yaml.parse(fs.readFileSync(workflowFilePath, 'utf8')); +const workflow = yaml.parse(fs.readFileSync(workflowFilePath, 'utf8')) as YamlWorkflow; const workflowJobs = parseWorkflowFile(workflow); const mockFileContent = getMockFileContent(workflowName, workflowJobs); diff --git a/workflow_tests/utils/utils.ts b/workflow_tests/utils/utils.ts index 494f830fb744..1fd60e3f92bc 100644 --- a/workflow_tests/utils/utils.ts +++ b/workflow_tests/utils/utils.ts @@ -170,7 +170,7 @@ function setJobRunners(act: ExtendedAct, jobs: Record, workflowP return act; } - const workflow: Workflow = yaml.parse(fs.readFileSync(workflowPath, 'utf8')); + const workflow = yaml.parse(fs.readFileSync(workflowPath, 'utf8')) as Workflow; Object.entries(jobs).forEach(([jobId, runner]) => { const job = workflow.jobs[jobId]; job['runs-on'] = runner; From 680070a54cb35403a944f00080d2c2fe0499f82b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:46:00 +0200 Subject: [PATCH 119/512] Change assertion from any to a random actionName --- tests/utils/collections/reportActions.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index dc3e9d8427b5..b078420230f5 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -5,7 +5,7 @@ import type {ReportAction} from '@src/types/onyx'; import type {ActionName} from '@src/types/onyx/OriginalMessage'; import type DeepRecord from '@src/types/utils/DeepRecord'; -const flattenActionNamesValues = (actionNames: DeepRecord) => { +const flattenActionNamesValues = (actionNames: DeepRecord): ActionName[] => { let result: ActionName[] = []; Object.values(actionNames).forEach((value) => { if (typeof value === 'object') { @@ -35,9 +35,10 @@ const deprecatedReportActions: ActionName[] = [ export default function createRandomReportAction(index: number): ReportAction { return { - // we need to add any here because of the way we are generating random values - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actionName: rand(flattenActionNamesValues(CONST.REPORT.ACTIONS.TYPE).filter((actionType: ActionName) => !deprecatedReportActions.includes(actionType))) as any, + // We need to assert the type of actionName so that rest of the properties are inferred correctly + actionName: rand( + flattenActionNamesValues(CONST.REPORT.ACTIONS.TYPE).filter((actionType: ActionName) => !deprecatedReportActions.includes(actionType)), + ) as typeof CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, reportActionID: index.toString(), previousReportActionID: (index === 0 ? 0 : index - 1).toString(), actorAccountID: index, From af0beb84d1cb32ed94f4a53d9b04355f1120f44b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:46:22 +0200 Subject: [PATCH 120/512] Add type to Electron download event listener --- desktop/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/main.ts b/desktop/main.ts index 6ab0bc6579d7..cb541ca87279 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -617,7 +617,7 @@ const mainWindow = (): Promise => { }); const downloadQueue = createDownloadQueue(); - ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData: DownloadItem) => { const downloadItem: DownloadItem = { ...downloadData, win: browserWindow, From 1c1b9c4c42706750ee977e0358d6310c886ed9ad Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:47:11 +0200 Subject: [PATCH 121/512] Remove unnecessary eslint no-unsafe-return comments --- src/components/FlatList/index.tsx | 1 - src/libs/Navigation/linkingConfig/index.ts | 1 - src/styles/utils/index.ts | 61 +++++++--------------- 3 files changed, 19 insertions(+), 44 deletions(-) diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx index 9f42e9597c79..31909ae0e32d 100644 --- a/src/components/FlatList/index.tsx +++ b/src/components/FlatList/index.tsx @@ -52,7 +52,6 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false return horizontal ? getScrollableNode(scrollRef.current)?.scrollLeft ?? 0 : getScrollableNode(scrollRef.current)?.scrollTop ?? 0; }, [horizontal]); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return const getContentView = useCallback(() => getScrollableNode(scrollRef.current)?.childNodes[0], []); const scrollToOffset = useCallback( diff --git a/src/libs/Navigation/linkingConfig/index.ts b/src/libs/Navigation/linkingConfig/index.ts index 64a40a224495..1f556aa67809 100644 --- a/src/libs/Navigation/linkingConfig/index.ts +++ b/src/libs/Navigation/linkingConfig/index.ts @@ -12,7 +12,6 @@ const linkingConfig: LinkingOptions = { const {adaptedState} = getAdaptedStateFromPath(...args); // ResultState | undefined is the type this function expect. - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return adaptedState; }, subscribe, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 44c40e17d60e..c3db1ee40865 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1232,16 +1232,12 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ */ getAutoGrowHeightInputStyle: (textInputHeight: number, maxHeight: number): ViewStyle => { if (textInputHeight > maxHeight) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.pr0, ...styles.overflowAuto, }; } - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.pr0, ...styles.overflowHidden, @@ -1270,17 +1266,11 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getBadgeColorStyle: (isSuccess: boolean, isError: boolean, isPressed = false, isAdHoc = false): ViewStyle => { if (isSuccess) { if (isAdHoc) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isPressed ? styles.badgeAdHocSuccessPressed : styles.badgeAdHocSuccess; } - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isPressed ? styles.badgeSuccessPressed : styles.badgeSuccess; } if (isError) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isPressed ? styles.badgeDangerPressed : styles.badgeDanger; } return {}; @@ -1357,8 +1347,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ ...styles.cursorDisabled, }; - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.link, ...(isDisabled ? disabledLinkStyles : {}), @@ -1424,8 +1412,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getGoogleListViewStyle: (shouldDisplayBorder: boolean): ViewStyle => { if (shouldDisplayBorder) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { ...styles.borderTopRounded, ...styles.borderBottomRounded, @@ -1491,35 +1477,29 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ /** * Generate the wrapper styles for the mini ReportActionContextMenu. */ - getMiniReportActionContextMenuWrapperStyle: (isReportActionItemGrouped: boolean): ViewStyle => - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - ({ - ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), - ...positioning.r4, - ...styles.cursorDefault, - ...styles.userSelectNone, - overflowAnchor: 'none', - position: 'absolute', - zIndex: 8, - }), + getMiniReportActionContextMenuWrapperStyle: (isReportActionItemGrouped: boolean): ViewStyle => ({ + ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), + ...positioning.r4, + ...styles.cursorDefault, + ...styles.userSelectNone, + overflowAnchor: 'none', + position: 'absolute', + zIndex: 8, + }), /** * Generate the styles for the ReportActionItem wrapper view. */ - getReportActionItemStyle: (isHovered = false, isClickable = false): ViewStyle => - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - ({ - display: 'flex', - justifyContent: 'space-between', - backgroundColor: isHovered - ? theme.hoverComponentBG - : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android - theme.transparent, - opacity: 1, - ...(isClickable ? styles.cursorPointer : styles.cursorInitial), - }), + getReportActionItemStyle: (isHovered = false, isClickable = false): ViewStyle => ({ + display: 'flex', + justifyContent: 'space-between', + backgroundColor: isHovered + ? theme.hoverComponentBG + : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android + theme.transparent, + opacity: 1, + ...(isClickable ? styles.cursorPointer : styles.cursorInitial), + }), /** * Determines the theme color for a modal based on the app's background color, @@ -1543,12 +1523,9 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getZoomCursorStyle: (isZoomed: boolean, isDragging: boolean): ViewStyle => { if (!isZoomed) { - // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return styles.cursorZoomIn; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return isDragging ? styles.cursorGrabbing : styles.cursorZoomOut; }, From cc84252a480d09a1b003a1fc29c24a7de0decec0 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:48:12 +0200 Subject: [PATCH 122/512] Fix jest.requireActual usage --- .../utils/__mocks__/index.ts | 2 +- tests/actions/OnyxUpdateManagerTest.ts | 2 +- tests/perf-test/ChatFinderPage.perf-test.tsx | 6 +++--- tests/perf-test/OptionsListUtils.perf-test.ts | 4 ++-- .../ReportActionCompose.perf-test.tsx | 20 ++++++++----------- .../perf-test/ReportActionsList.perf-test.tsx | 4 ++-- tests/perf-test/ReportScreen.perf-test.tsx | 8 ++++---- tests/ui/UnreadIndicatorsTest.tsx | 4 ++-- tests/utils/LHNTestUtils.tsx | 6 +++--- 9 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts index b4d97a4399db..f66e059ff7f6 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts @@ -3,7 +3,7 @@ import createProxyForObject from '@src/utils/createProxyForObject'; import type * as OnyxUpdateManagerUtilsImport from '..'; import {applyUpdates} from './applyUpdates'; -const UtilsImplementation: typeof OnyxUpdateManagerUtilsImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils'); +const UtilsImplementation = jest.requireActual('@libs/actions/OnyxUpdateManager/utils'); type OnyxUpdateManagerUtilsMockValues = { onValidateAndApplyDeferredUpdates: ((clientLastUpdateID?: number) => Promise) | undefined; diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index d1a10f8a4775..3a4ff0779217 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -20,7 +20,7 @@ import createOnyxMockUpdate from '../utils/createOnyxMockUpdate'; jest.mock('@libs/actions/App'); jest.mock('@libs/actions/OnyxUpdateManager/utils'); jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates', () => { - const ApplyUpdatesImplementation: typeof ApplyUpdatesImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); + const ApplyUpdatesImplementation = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); return { applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => ApplyUpdatesImplementation.applyUpdates(updates)), diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/ChatFinderPage.perf-test.tsx index 55430b2a9d48..fda81265bdc0 100644 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ b/tests/perf-test/ChatFinderPage.perf-test.tsx @@ -27,7 +27,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; jest.mock('lodash/debounce', () => - jest.fn((fn: Record>) => { + jest.fn((fn: Record) => { // eslint-disable-next-line no-param-reassign fn.cancel = jest.fn(); return fn; @@ -50,7 +50,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ })); jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); + const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useFocusEffect: jest.fn(), @@ -67,7 +67,7 @@ jest.mock('@react-navigation/native', () => { getCurrentRoute: () => jest.fn(), getState: () => jest.fn(), }), - } as typeof NativeNavigation; + }; }); jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { diff --git a/tests/perf-test/OptionsListUtils.perf-test.ts b/tests/perf-test/OptionsListUtils.perf-test.ts index ddd441f8fae2..16522297a416 100644 --- a/tests/perf-test/OptionsListUtils.perf-test.ts +++ b/tests/perf-test/OptionsListUtils.perf-test.ts @@ -64,13 +64,13 @@ const mockedPersonalDetailsMap = getMockedPersonalDetails(PERSONAL_DETAILS_LIST_ const mockedBetas = Object.values(CONST.BETAS); jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); + const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, createNavigationContainerRef: () => ({ getState: () => jest.fn(), }), - } as typeof NativeNavigation; + }; }); const options = OptionsListUtils.createOptionList(personalDetails, reports); diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 28987e6b58ed..eee372ceb659 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -21,17 +21,13 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // mock PortalStateContext jest.mock('@gorhom/portal'); -jest.mock( - 'react-native-reanimated', - () => - ({ - ...jest.requireActual('react-native-reanimated/mock'), - useAnimatedRef: jest.fn(), - } as typeof Animated), -); +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + useAnimatedRef: jest.fn(), +})); jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); + const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useNavigation: () => ({ @@ -40,11 +36,11 @@ jest.mock('@react-navigation/native', () => { }), useIsFocused: () => true, useNavigationState: () => {}, - } as typeof Navigation; + }; }); jest.mock('@src/libs/actions/EmojiPickerAction', () => { - const actualEmojiPickerAction = jest.requireActual('@src/libs/actions/EmojiPickerAction'); + const actualEmojiPickerAction = jest.requireActual('@src/libs/actions/EmojiPickerAction'); return { ...actualEmojiPickerAction, emojiPickerRef: { @@ -55,7 +51,7 @@ jest.mock('@src/libs/actions/EmojiPickerAction', () => { showEmojiPicker: jest.fn(), hideEmojiPicker: jest.fn(), isActive: () => true, - } as EmojiPickerRef; + }; }); jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType) => { diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx index 17b27c6905cc..85a4df7f307e 100644 --- a/tests/perf-test/ReportActionsList.perf-test.tsx +++ b/tests/perf-test/ReportActionsList.perf-test.tsx @@ -53,12 +53,12 @@ jest.mock('@components/withCurrentUserPersonalDetails', () => { }); jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); + const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useRoute: () => mockedNavigate, useIsFocused: () => true, - } as typeof Navigation; + }; }); jest.mock('@src/components/ConfirmedRoute.tsx'); diff --git a/tests/perf-test/ReportScreen.perf-test.tsx b/tests/perf-test/ReportScreen.perf-test.tsx index d452e9412655..c960af0c46f0 100644 --- a/tests/perf-test/ReportScreen.perf-test.tsx +++ b/tests/perf-test/ReportScreen.perf-test.tsx @@ -42,13 +42,13 @@ jest.mock('@src/libs/API', () => ({ })); jest.mock('react-native-reanimated', () => { - const actualNav = jest.requireActual('react-native-reanimated/mock'); + const actualNav = jest.requireActual('react-native-reanimated/mock'); return { ...actualNav, useSharedValue: jest.fn, useAnimatedStyle: jest.fn, useAnimatedRef: jest.fn, - } as typeof Animated; + }; }); jest.mock('@src/components/ConfirmedRoute.tsx'); @@ -90,7 +90,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ })); jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); + const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useFocusEffect: jest.fn(), @@ -102,7 +102,7 @@ jest.mock('@react-navigation/native', () => { }), useNavigationState: () => {}, createNavigationContainerRef: jest.fn(), - } as typeof Navigation; + }; }); // mock PortalStateContext diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index a9318dff217a..e849e942938c 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -91,7 +91,7 @@ const createAddListenerMock = (): ListenerMock => { }; jest.mock('@react-navigation/native', () => { - const actualNav: jest.Mocked = jest.requireActual('@react-navigation/native'); + const actualNav = jest.requireActual('@react-navigation/native'); const {triggerTransitionEnd, addListener} = createAddListenerMock(); transitionEndCB = triggerTransitionEnd; @@ -110,7 +110,7 @@ jest.mock('@react-navigation/native', () => { getState: () => ({ routes: [], }), - } as typeof NativeNavigation; + }; }); beforeAll(() => { diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 89c31d92843e..7a9977138227 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -33,8 +33,8 @@ type MockedSidebarLinksProps = { currentReportID?: string; }; -jest.mock('@react-navigation/native', (): typeof Navigation => { - const actualNav = jest.requireActual('@react-navigation/native'); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useRoute: jest.fn(), @@ -45,7 +45,7 @@ jest.mock('@react-navigation/native', (): typeof Navigation => { addListener: jest.fn(), }), createNavigationContainerRef: jest.fn(), - } as typeof Navigation; + }; }); const fakePersonalDetails: PersonalDetailsList = { From 4b6d7d45de7974fe23b9f1b2921d81a600dba7aa Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 14:52:17 +0200 Subject: [PATCH 123/512] Add assertion to other Json.parse --- src/components/IFrame.tsx | 2 +- src/libs/Notification/PushNotification/index.native.ts | 4 ++-- .../PushNotification/shouldShowPushNotification.ts | 4 ++-- .../Notification/clearReportNotifications/index.native.ts | 6 +++--- src/libs/Pusher/pusher.ts | 2 +- tests/unit/sanitizeStringForJSONParseTest.ts | 7 +++---- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/IFrame.tsx b/src/components/IFrame.tsx index 05da3a1edb9c..f492df0f3866 100644 --- a/src/components/IFrame.tsx +++ b/src/components/IFrame.tsx @@ -17,7 +17,7 @@ function getNewDotURL(url: string): string { let params: Record; try { - params = JSON.parse(paramString); + params = JSON.parse(paramString) as Record; } catch { params = {}; } diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 34699f0610e1..72b36fdc33f7 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -1,4 +1,4 @@ -import type {PushPayload} from '@ua/react-native-airship'; +import type {JsonValue, PushPayload} from '@ua/react-native-airship'; import Airship, {EventType} from '@ua/react-native-airship'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; @@ -31,7 +31,7 @@ function pushNotificationEventCallback(eventType: EventType, notification: PushP // On Android, some notification payloads are sent as a JSON string rather than an object if (typeof payload === 'string') { - payload = JSON.parse(payload); + payload = JSON.parse(payload) as JsonValue; } const data = payload as PushNotificationData; diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index fd6029857ded..f66638657a19 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,4 +1,4 @@ -import type {PushPayload} from '@ua/react-native-airship'; +import type {JsonValue, PushPayload} from '@ua/react-native-airship'; import Log from '@libs/Log'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; @@ -14,7 +14,7 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo // The payload is string encoded on Android if (typeof payload === 'string') { - payload = JSON.parse(payload); + payload = JSON.parse(payload) as JsonValue; } const data = payload as PushNotificationData; diff --git a/src/libs/Notification/clearReportNotifications/index.native.ts b/src/libs/Notification/clearReportNotifications/index.native.ts index 3485df2d5ade..751cac3849e1 100644 --- a/src/libs/Notification/clearReportNotifications/index.native.ts +++ b/src/libs/Notification/clearReportNotifications/index.native.ts @@ -1,4 +1,4 @@ -import type {PushPayload} from '@ua/react-native-airship'; +import type {JsonValue, PushPayload} from '@ua/react-native-airship'; import Airship from '@ua/react-native-airship'; import Log from '@libs/Log'; import type {PushNotificationData} from '@libs/Notification/PushNotification/NotificationType'; @@ -8,7 +8,7 @@ import type ClearReportNotifications from './types'; const parseNotificationAndReportIDs = (pushPayload: PushPayload) => { let payload = pushPayload.extras.payload; if (typeof payload === 'string') { - payload = JSON.parse(payload); + payload = JSON.parse(payload) as JsonValue; } const data = payload as PushNotificationData; return { @@ -34,7 +34,7 @@ const clearReportNotifications: ClearReportNotifications = (reportID: string) => Log.info(`[PushNotification] found ${reportNotificationIDs.length} notifications to clear`, false, {reportID}); reportNotificationIDs.forEach((notificationID) => Airship.push.clearNotification(notificationID)); }) - .catch((error) => { + .catch((error: Error) => { Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} [PushNotification] BrowserNotifications.clearReportNotifications threw an error. This should never happen.`, {reportID, error}); }); }; diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index 35864d1b6f2e..a092869cdbdc 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -170,7 +170,7 @@ function bindEventToChannel(channel: Channel let data: EventData; try { - data = isObject(eventData) ? eventData : JSON.parse(eventData as string); + data = isObject(eventData) ? eventData : (JSON.parse(eventData) as EventData); } catch (err) { Log.alert('[Pusher] Unable to parse single JSON event data from Pusher', {error: err, eventData}); return; diff --git a/tests/unit/sanitizeStringForJSONParseTest.ts b/tests/unit/sanitizeStringForJSONParseTest.ts index e269617d4f24..da09aa346db9 100644 --- a/tests/unit/sanitizeStringForJSONParseTest.ts +++ b/tests/unit/sanitizeStringForJSONParseTest.ts @@ -41,16 +41,15 @@ describe('santizeStringForJSONParse', () => { describe.each(invalidJSONData)('canHandleInvalidJSON', (input, expectedOutput) => { test('sanitizeStringForJSONParse', () => { const badJSON = `{"key": "${input}"}`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- it's supposed to throw an error - expect(() => JSON.parse(badJSON)).toThrow(); - const goodJSON: ParsedJSON = JSON.parse(`{"key": "${sanitizeStringForJSONParse(input)}"}`); + expect(() => JSON.parse(badJSON) as unknown).toThrow(); + const goodJSON = JSON.parse(`{"key": "${sanitizeStringForJSONParse(input)}"}`) as ParsedJSON; expect(goodJSON.key).toStrictEqual(expectedOutput); }); }); describe.each(validJSONData)('canHandleValidJSON', (input, expectedOutput) => { test('sanitizeStringForJSONParse', () => { - const goodJSON: ParsedJSON = JSON.parse(`{"key": "${sanitizeStringForJSONParse(input)}"}`); + const goodJSON = JSON.parse(`{"key": "${sanitizeStringForJSONParse(input)}"}`) as ParsedJSON; expect(goodJSON.key).toStrictEqual(expectedOutput); }); }); From 582a0b4f91e3730fa7a2b54ff5f145ecf2d5ddc8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 15:00:02 +0200 Subject: [PATCH 124/512] Add generic types to requires --- tests/unit/markPullRequestsAsDeployedTest.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/unit/markPullRequestsAsDeployedTest.ts b/tests/unit/markPullRequestsAsDeployedTest.ts index 8d8b25968a28..bf90921452ba 100644 --- a/tests/unit/markPullRequestsAsDeployedTest.ts +++ b/tests/unit/markPullRequestsAsDeployedTest.ts @@ -138,7 +138,7 @@ beforeAll(() => { jest.mock('../../.github/libs/ActionUtils', () => ({ getJSONInput: jest.fn().mockImplementation((name: string, defaultValue: string) => { try { - const input: string = mockGetInput(name); + const input = mockGetInput(name) as string; return JSON.parse(input) as unknown; } catch (err) { return defaultValue; @@ -171,10 +171,12 @@ afterAll(() => { jest.clearAllMocks(); }); +type MockedActionRun = () => Promise; + describe('markPullRequestsAsDeployed', () => { it('comments on pull requests correctly for a standard staging deploy', async () => { // Note: we import this in here so that it executes after all the mocks are set up - run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); + run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); await run(); expect(mockCreateComment).toHaveBeenCalledTimes(Object.keys(PRList).length); for (let i = 0; i < Object.keys(PRList).length; i++) { @@ -204,7 +206,7 @@ platform | result }); // Note: we import this in here so that it executes after all the mocks are set up - run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); + run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); await run(); expect(mockCreateComment).toHaveBeenCalledTimes(Object.keys(PRList).length); @@ -260,7 +262,7 @@ platform | result }); // Note: we import this in here so that it executes after all the mocks are set up - run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); + run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); await run(); expect(mockCreateComment).toHaveBeenCalledTimes(1); expect(mockCreateComment).toHaveBeenCalledWith({ @@ -295,7 +297,7 @@ platform | result }); // Note: we import this in here so that it executes after all the mocks are set up - run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); + run = require('../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed'); await run(); expect(mockCreateComment).toHaveBeenCalledTimes(Object.keys(PRList).length); for (let i = 0; i < Object.keys(PRList).length; i++) { From a7fee2d833cde8c01ea57a5560b9b75390165712 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 15:32:52 +0200 Subject: [PATCH 125/512] Mock middleware properly --- tests/unit/RequestTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/RequestTest.ts b/tests/unit/RequestTest.ts index bb444717ad41..74c42cdf02c4 100644 --- a/tests/unit/RequestTest.ts +++ b/tests/unit/RequestTest.ts @@ -18,8 +18,8 @@ const request: OnyxTypes.Request = { }; test('Request.use() can register a middleware and it will run', () => { - const testMiddleware = jest.fn(); - Request.use(testMiddleware); + const testMiddleware = jest.fn>(); + Request.use(testMiddleware as unknown as Middleware); Request.processWithMiddleware(request, true); return waitForBatchedUpdates().then(() => { From 14e458831a3c8df33e65f95b52aed48ac927a1a9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 15:34:44 +0200 Subject: [PATCH 126/512] Assert that prop is a string --- tests/ui/UnreadIndicatorsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index e849e942938c..41415f39fc4b 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -478,7 +478,7 @@ describe('Unread Indicators', () => { const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); - const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; + const reportActionID = unreadIndicator[0]?.props?.['data-action-id'] as string; expect(reportActionID).toBe('3'); // Scroll up and verify the new messages badge appears scrollUpToRevealNewMessagesBadge(); From c3cab6b66ea7914319199c29c396c28de5972b42 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 15:35:15 +0200 Subject: [PATCH 127/512] Add missing args to octokit mocked functions --- tests/unit/GithubUtilsTest.ts | 4 +++- tests/unit/createOrUpdateStagingDeployTest.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/GithubUtilsTest.ts b/tests/unit/GithubUtilsTest.ts index 0ff25d16d06c..b157f105f469 100644 --- a/tests/unit/GithubUtilsTest.ts +++ b/tests/unit/GithubUtilsTest.ts @@ -34,6 +34,8 @@ type ObjectMethodData = { data: T; }; +type OctokitCreateIssue = InternalOctokit['rest']['issues']['create']; + const asMutable = (value: T): Writable => value as Writable; beforeAll(() => { @@ -44,7 +46,7 @@ beforeAll(() => { const moctokit = { rest: { issues: { - create: jest.fn().mockImplementation((arg) => + create: jest.fn().mockImplementation((arg: Parameters[0]) => Promise.resolve({ data: { ...arg, diff --git a/tests/unit/createOrUpdateStagingDeployTest.ts b/tests/unit/createOrUpdateStagingDeployTest.ts index 59ebe9d639cf..8cd340f317c7 100644 --- a/tests/unit/createOrUpdateStagingDeployTest.ts +++ b/tests/unit/createOrUpdateStagingDeployTest.ts @@ -34,7 +34,7 @@ beforeAll(() => { const moctokit = { rest: { issues: { - create: jest.fn().mockImplementation((arg) => + create: jest.fn().mockImplementation((arg: Arguments) => Promise.resolve({ data: { ...arg, From 88de71a4370f4b47682dc5f5c56336747e839bf7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 16:06:34 +0200 Subject: [PATCH 128/512] Add type for backgroundColor --- src/styles/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index b031e665594f..fd81203a428a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -784,7 +784,7 @@ const styles = (theme: ThemeColors) => height: 140, }, - pickerSmall: (disabled = false, backgroundColor = theme.highlightBG) => + pickerSmall: (disabled = false, backgroundColor: string = theme.highlightBG) => ({ inputIOS: { fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, @@ -1283,7 +1283,7 @@ const styles = (theme: ThemeColors) => zIndex: 1, }, - picker: (disabled = false, backgroundColor = theme.appBG) => + picker: (disabled = false, backgroundColor: string = theme.appBG) => ({ iconContainer: { top: Math.round(variables.inputHeight * 0.5) - 11, @@ -1546,7 +1546,7 @@ const styles = (theme: ThemeColors) => borderColor: theme.success, }, - statusIndicator: (backgroundColor = theme.danger) => + statusIndicator: (backgroundColor: string = theme.danger) => ({ borderColor: theme.sidebar, backgroundColor, @@ -1560,7 +1560,7 @@ const styles = (theme: ThemeColors) => zIndex: 10, } satisfies ViewStyle), - bottomTabStatusIndicator: (backgroundColor = theme.danger) => ({ + bottomTabStatusIndicator: (backgroundColor: string = theme.danger) => ({ borderColor: theme.sidebar, backgroundColor, borderRadius: 8, From 1f3cd34c6b723b11192e5315db25085112672b7a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 16:06:57 +0200 Subject: [PATCH 129/512] Fix minor type issues --- src/components/MagicCodeInput.tsx | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 5 ++--- tests/ui/UnreadIndicatorsTest.tsx | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 6239243cb5ab..956f3ffe5e02 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -298,7 +298,7 @@ function MagicCodeInput( // Fill the array with empty characters if there are no inputs. if (focusedIndex === 0 && !hasInputs) { - numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); // Deletes the value of the previous input and focuses on it. } else if (focusedIndex && focusedIndex !== 0) { diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 6ed851c70f4e..35d725c66e17 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -64,9 +64,8 @@ type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & * Inverts an object, equivalent of _.invert */ function invertObject(object: Record): Record { - const invertedEntries = Object.entries(object).map(([key, value]) => [value, key]); - const inverted: Record = Object.fromEntries(invertedEntries); - return inverted; + const invertedEntries = Object.entries(object).map(([key, value]) => [value, key] as const); + return Object.fromEntries(invertedEntries); } type MemberOption = Omit & {accountID: number}; diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 41415f39fc4b..2b1cabae5a80 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -321,7 +321,7 @@ describe('Unread Indicators', () => { const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); - const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; + const reportActionID = unreadIndicator[0]?.props?.['data-action-id'] as string; expect(reportActionID).toBe('4'); // Scroll up and verify that the "New messages" badge appears scrollUpToRevealNewMessagesBadge(); From 1141173811a89f86e948033430d8cce315a80a16 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 16:08:54 +0200 Subject: [PATCH 130/512] Augment objectContaining and arrayContaining to return unknown instead of any --- src/types/modules/jest.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/types/modules/jest.d.ts diff --git a/src/types/modules/jest.d.ts b/src/types/modules/jest.d.ts new file mode 100644 index 000000000000..532437d2f7cf --- /dev/null +++ b/src/types/modules/jest.d.ts @@ -0,0 +1,13 @@ +declare global { + namespace jest { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Expect { + // eslint-disable-next-line @typescript-eslint/ban-types + objectContaining(obj: E): unknown; + arrayContaining(arr: readonly E[]): unknown; + } + } +} + +// We used the export {} line to mark this file as an external module +export {}; From 31f64f432da9afb22aee9d87aa6970f38c56e03d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 16:21:54 +0200 Subject: [PATCH 131/512] Fix minor errors --- __mocks__/@ua/react-native-airship.ts | 16 ++++++++-------- __mocks__/fs.ts | 1 + __mocks__/react-native.ts | 4 ++-- desktop/main.ts | 2 +- src/components/OfflineWithFeedback.tsx | 2 +- src/components/Tooltip/PopoverAnchorTooltip.tsx | 2 +- src/libs/E2E/utils/NetworkInterceptor.ts | 2 +- src/libs/Navigation/linkTo/index.ts | 2 +- src/libs/OptionsListUtils.ts | 4 ++-- src/libs/actions/Session/index.ts | 2 +- src/libs/fileDownload/index.ios.ts | 2 +- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 2 +- src/utils/createProxyForObject.ts | 2 +- tests/perf-test/ReportActionsUtils.perf-test.ts | 4 ++-- 14 files changed, 24 insertions(+), 23 deletions(-) diff --git a/__mocks__/@ua/react-native-airship.ts b/__mocks__/@ua/react-native-airship.ts index ae7661ab672f..14909b58b31c 100644 --- a/__mocks__/@ua/react-native-airship.ts +++ b/__mocks__/@ua/react-native-airship.ts @@ -15,31 +15,31 @@ const iOS: Partial = { }, }; -const pushIOS: AirshipPushIOS = jest.fn().mockImplementation(() => ({ +const pushIOS = jest.fn().mockImplementation(() => ({ setBadgeNumber: jest.fn(), setForegroundPresentationOptions: jest.fn(), setForegroundPresentationOptionsCallback: jest.fn(), -}))(); +}))() as AirshipPushIOS; -const pushAndroid: AirshipPushAndroid = jest.fn().mockImplementation(() => ({ +const pushAndroid = jest.fn().mockImplementation(() => ({ setForegroundDisplayPredicate: jest.fn(), -}))(); +}))() as AirshipPushAndroid; -const push: AirshipPush = jest.fn().mockImplementation(() => ({ +const push = jest.fn().mockImplementation(() => ({ iOS: pushIOS, android: pushAndroid, enableUserNotifications: () => Promise.resolve(false), clearNotifications: jest.fn(), getNotificationStatus: () => Promise.resolve({airshipOptIn: false, systemEnabled: false, airshipEnabled: false}), getActiveNotifications: () => Promise.resolve([]), -}))(); +}))() as AirshipPush; -const contact: AirshipContact = jest.fn().mockImplementation(() => ({ +const contact = jest.fn().mockImplementation(() => ({ identify: jest.fn(), getNamedUserId: () => Promise.resolve(undefined), reset: jest.fn(), module: jest.fn(), -}))(); +}))() as AirshipContact; const Airship: Partial = { addListener: jest.fn(), diff --git a/__mocks__/fs.ts b/__mocks__/fs.ts index cca0aa9520ec..3f8579557c82 100644 --- a/__mocks__/fs.ts +++ b/__mocks__/fs.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ const {fs} = require('memfs'); module.exports = fs; diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 27b78b308446..f0cd99825c3d 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -41,7 +41,7 @@ jest.doMock('react-native', () => { }; }; - const reactNativeMock: ReactNativeMock = Object.setPrototypeOf( + const reactNativeMock = Object.setPrototypeOf( { NativeModules: { ...ReactNative.NativeModules, @@ -102,7 +102,7 @@ jest.doMock('react-native', () => { }, }, ReactNative, - ); + ) as ReactNativeMock; return reactNativeMock; }); diff --git a/desktop/main.ts b/desktop/main.ts index cb541ca87279..2c802b1e590a 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -141,7 +141,7 @@ const manuallyCheckForUpdates = (menuItem?: MenuItem, browserWindow?: BrowserWin autoUpdater .checkForUpdates() - .catch((error) => { + .catch((error: Error) => { isSilentUpdating = false; return {error}; }) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 70354c4a4676..c12e73280c7d 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -106,7 +106,7 @@ function OfflineWithFeedback({ return child; } - const childProps: {children: React.ReactNode | undefined; style: AllStyles} = child.props; + const childProps = child.props as {children: React.ReactNode | undefined; style: AllStyles}; const props: StrikethroughProps = { style: StyleUtils.combineStyles(childProps.style, styles.offlineFeedback.deleted, styles.userSelectNone), }; diff --git a/src/components/Tooltip/PopoverAnchorTooltip.tsx b/src/components/Tooltip/PopoverAnchorTooltip.tsx index 693de83fa5d7..7b6ebfd14258 100644 --- a/src/components/Tooltip/PopoverAnchorTooltip.tsx +++ b/src/components/Tooltip/PopoverAnchorTooltip.tsx @@ -10,7 +10,7 @@ function PopoverAnchorTooltip({shouldRender = true, children, ...props}: Tooltip const isPopoverRelatedToTooltipOpen = useMemo(() => { // eslint-disable-next-line @typescript-eslint/dot-notation - const tooltipNode: Node | null = tooltipRef.current?.['_childNode'] ?? null; + const tooltipNode = (tooltipRef.current?.['_childNode'] as Node) ?? null; if ( isOpen && diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index ad23afeb0c3b..511c8014f0cd 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -26,7 +26,7 @@ function getFetchRequestHeadersAsObject(fetchRequest: RequestInit): Record { - headers[key] = value; + headers[key] = value as string; }); } return headers; diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index 337dbb2b6088..262975cac298 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -69,7 +69,7 @@ export default function linkTo(navigation: NavigationContainerRef): Category[] { const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); // An object that respects nesting of categories. Also, can contain only uniq categories. - const hierarchy = {}; + const hierarchy: Hierarchy = {}; /** * Iterates over all categories to set each category in a proper place in hierarchy * It gets a path based on a category name e.g. "Parent: Child: Subcategory" -> "Parent.Child.Subcategory". @@ -978,7 +978,7 @@ function sortCategories(categories: Record): Category[] { */ sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); - const existedValue = lodashGet(hierarchy, path, {}); + const existedValue = lodashGet(hierarchy, path, {}) as Hierarchy; lodashSet(hierarchy, path, { ...existedValue, name: category.name, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index e85fdc9d1531..8c931c1171f1 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -768,7 +768,7 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha Log.info('[PusherAuthorizer] Pusher authenticated successfully', false, {channelName}); callback(null, response as ChannelAuthorizationData); }) - .catch((error) => { + .catch((error: Error) => { Log.hmmm('[PusherAuthorizer] Unhandled error: ', {channelName, error}); callback(new Error('AuthenticatePusher request failed'), {auth: ''}); }); diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts index 0e6701dbda3a..b1617bb440d0 100644 --- a/src/libs/fileDownload/index.ios.ts +++ b/src/libs/fileDownload/index.ios.ts @@ -44,7 +44,7 @@ function downloadVideo(fileUrl: string, fileName: string): Promise { - documentPathUri = attachment.data; + documentPathUri = attachment.data as string | null; if (!documentPathUri) { throw new Error('Error downloading video'); } diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 02bb5dd99687..2f0f93cbd8b2 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -122,7 +122,7 @@ function ExpensifyCardPage({ [revealedCardID]: '', })); }) - .catch((error) => { + .catch((error: string) => { setCardsDetailsErrors((prevState) => ({ ...prevState, [revealedCardID]: error, diff --git a/src/utils/createProxyForObject.ts b/src/utils/createProxyForObject.ts index c18e5e30a0d9..c5055845d5da 100644 --- a/src/utils/createProxyForObject.ts +++ b/src/utils/createProxyForObject.ts @@ -12,7 +12,7 @@ const createProxyForObject = >(value: Valu return target[property]; }, - set: (target, property, newValue) => { + set: (target, property, newValue: Value[string]) => { if (typeof property === 'symbol') { return false; } diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index f194cd32bbf4..a33a448cfee7 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -20,13 +20,13 @@ const getMockedReportActionsMap = (reportsLength = 10, actionsPerReportLength = const reportKeysMap = Array.from({length: reportsLength}, (v, i) => { const key = i + 1; - return {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`]: Object.assign({}, ...mockReportActions)}; + return {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`]: Object.assign({}, ...mockReportActions) as Partial}; }); return Object.assign({}, ...reportKeysMap) as Partial; }; -const mockedReportActionsMap = getMockedReportActionsMap(2, 10000); +const mockedReportActionsMap: Partial = getMockedReportActionsMap(2, 10000); const reportActions = createCollection( (item) => `${item.reportActionID}`, From 3df366995ca66db9a36100a00b857f7845cdee4f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Jun 2024 16:25:31 +0200 Subject: [PATCH 132/512] Add more assertions --- src/components/AttachmentModal.tsx | 2 +- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- src/components/Hoverable/ActiveHoverable.tsx | 4 ++-- src/components/Pressable/PressableWithDelayToggle.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index c56163a3886f..9624fb493547 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -321,7 +321,7 @@ function AttachmentModal({ } let fileObject = data; if ('getAsFile' in data && typeof data.getAsFile === 'function') { - fileObject = data.getAsFile(); + fileObject = data.getAsFile() as FileObject; } if (!fileObject) { return; diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 947569538d32..0c267ead673a 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -121,7 +121,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, return; } - const item: Attachment = entry.item; + const item = entry.item as Attachment; if (entry.index !== null) { setPage(entry.index); setActiveSource(item.source); diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index abd48d432953..fd3d4f3d19e8 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -48,7 +48,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez return; } - const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { + const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling: boolean) => { isScrollingRef.current = scrolling; if (!isScrollingRef.current) { setIsHovered(isHoveredRef.current); @@ -102,7 +102,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez const child = useMemo(() => getReturnValue(children, !isScrollingRef.current && isHovered), [children, isHovered]); - const {onMouseEnter, onMouseLeave, onMouseMove, onBlur}: OnMouseEvents = child.props; + const {onMouseEnter, onMouseLeave, onMouseMove, onBlur} = child.props as OnMouseEvents; const hoverAndForwardOnMouseEnter = useCallback( (e: MouseEvent) => { diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index 86f6c9d8aff8..617811637525 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -99,7 +99,7 @@ function PressableWithDelayToggle( return ( Date: Wed, 19 Jun 2024 11:13:14 -0700 Subject: [PATCH 133/512] Remove empty file --- src/components/KeyboardAvoidingView/types.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/components/KeyboardAvoidingView/types.ts diff --git a/src/components/KeyboardAvoidingView/types.ts b/src/components/KeyboardAvoidingView/types.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From 40a3a6a606e0c7159931bcb48205859a0aec6504 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 19 Jun 2024 11:23:55 -0700 Subject: [PATCH 134/512] Make react-is a dev dependency --- package-lock.json | 8 ++++---- package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3cc19babfb55..292400aca6c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,6 @@ "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", "react-fast-pdf": "1.0.13", - "react-is": "18.2.0", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -233,6 +232,7 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", + "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", @@ -31618,9 +31618,9 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react-map-gl": { "version": "7.1.3", diff --git a/package.json b/package.json index 7b55e5896af3..00826a50a622 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,6 @@ "process": "^0.11.10", "pusher-js": "8.3.0", "react": "18.2.0", - "react-is": "18.2.0", "react-beautiful-dnd": "^13.1.1", "react-collapse": "^5.1.0", "react-content-loader": "^7.0.0", @@ -231,10 +230,10 @@ "@types/node": "^20.11.5", "@types/pusher-js": "^5.1.0", "@types/react": "18.2.45", - "@types/react-is": "18.2.0", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-collapse": "^5.0.1", "@types/react-dom": "^18.2.4", + "@types/react-is": "18.2.0", "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", @@ -285,6 +284,7 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", + "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", From bf3ef1d709ee82e5af37a05146e674d7bc251315 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 19 Jun 2024 17:36:04 -0400 Subject: [PATCH 135/512] Improve test mocks and assertions --- tests/ui/PaginationTest.tsx | 79 +++++++++++++++++++++++++------------ tests/utils/TestHelper.ts | 52 ++++++++++++++++++------ tsconfig.json | 1 + 3 files changed, 95 insertions(+), 37 deletions(-) diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 97b9ba885870..794c54d4a121 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -4,7 +4,6 @@ import {act, fireEvent, render, screen} from '@testing-library/react-native'; import {addSeconds, format, subMinutes} from 'date-fns'; import React from 'react'; import Onyx from 'react-native-onyx'; -import type {ApiCommand} from '@libs/API/types'; import * as Localize from '@libs/Localize'; import * as AppActions from '@userActions/App'; import * as User from '@userActions/User'; @@ -88,14 +87,17 @@ const USER_A_EMAIL = 'user_a@test.com'; const USER_B_ACCOUNT_ID = 2; const USER_B_EMAIL = 'user_b@test.com'; -function mockOpenReport(messageCount: number, includeCreatedAction: boolean) { +function buildReportComments(count: number, initialID: string, reverse = false) { + let currentID = parseInt(initialID, 10); const TEN_MINUTES_AGO = subMinutes(new Date(), 10); - const actions = Object.fromEntries( - Array.from({length: messageCount}).map((_, index) => { - const created = format(addSeconds(TEN_MINUTES_AGO, 10 * index), CONST.DATE.FNS_DB_FORMAT_STRING); + return Object.fromEntries( + Array.from({length: Math.min(count, currentID)}).map(() => { + const created = format(addSeconds(TEN_MINUTES_AGO, 10 * currentID), CONST.DATE.FNS_DB_FORMAT_STRING); + const id = currentID; + currentID += reverse ? 1 : -1; return [ - `${index + 1}`, - index === 0 && includeCreatedAction + `${id}`, + id === 1 ? { reportActionID: '1', actionName: 'CREATED' as const, @@ -107,23 +109,44 @@ function mockOpenReport(messageCount: number, includeCreatedAction: boolean) { }, ], } - : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${index + 1}`), + : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${id}`), ]; }), ); - fetchMock.mockAPICommand('OpenReport', [ +} + +function mockOpenReport(messageCount: number, initialID: string) { + fetchMock.mockAPICommand('OpenReport', () => [ { onyxMethod: 'merge', key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - value: actions, + value: buildReportComments(messageCount, initialID), }, ]); } -function expectAPICommandToHaveBeenCalled(commandName: ApiCommand, expectedCalls: number) { - expect(fetchMock.mock.calls.filter((c) => c[0] === `https://www.expensify.com.dev/api/${commandName}?`)).toHaveLength(expectedCalls); +function mockGetOlderActions(messageCount: number) { + fetchMock.mockAPICommand('GetOlderActions', ({reportActionID}) => [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + // The API also returns the action that was requested with the reportActionID. + value: buildReportComments(messageCount + 1, reportActionID), + }, + ]); } +// function mockGetNewerActions(messageCount: number) { +// fetchMock.mockAPICommand('GetNewerActions', ({reportActionID}) => [ +// { +// onyxMethod: 'merge', +// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, +// // The API also returns the action that was requested with the reportActionID. +// value: buildReportComments(messageCount + 1, reportActionID, true), +// }, +// ]); +// } + /** * Sets up a test with a logged in user. Returns the test instance. */ @@ -182,15 +205,16 @@ describe('Pagination', () => { }); it('opens a chat and load initial messages', async () => { - mockOpenReport(5, true); + mockOpenReport(5, '5'); await signInAndGetApp(); await navigateToSidebarOption(0); expect(getReportActions()).toHaveLength(5); - expectAPICommandToHaveBeenCalled('OpenReport', 1); - expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 1); + TestHelper.expectAPICommandToHaveBeenCalledWith('OpenReport', 0, {reportID: REPORT_ID, reportActionID: ''}); + TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); // Scrolling here should not trigger a new network request. scrollToOffset(LIST_CONTENT_SIZE.height); @@ -198,28 +222,31 @@ describe('Pagination', () => { scrollToOffset(0); await waitForBatchedUpdatesWithAct(); - expectAPICommandToHaveBeenCalled('OpenReport', 1); - expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 1); + TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); }); it('opens a chat and load older messages', async () => { - mockOpenReport(5, false); + mockOpenReport(5, '8'); + mockGetOlderActions(5); await signInAndGetApp(); await navigateToSidebarOption(0); expect(getReportActions()).toHaveLength(5); - expectAPICommandToHaveBeenCalled('OpenReport', 1); - expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 1); + TestHelper.expectAPICommandToHaveBeenCalledWith('OpenReport', 0, {reportID: REPORT_ID, reportActionID: ''}); + TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); // Scrolling here should trigger a new network request. scrollToOffset(LIST_CONTENT_SIZE.height); await waitForBatchedUpdatesWithAct(); - expectAPICommandToHaveBeenCalled('OpenReport', 1); - expectAPICommandToHaveBeenCalled('GetOlderActions', 1); - expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 1); + TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 1); + TestHelper.expectAPICommandToHaveBeenCalledWith('GetOlderActions', 0, {reportID: REPORT_ID, reportActionID: '4'}); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); }); }); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index f3aed289acff..81fe1e9173f4 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -2,6 +2,7 @@ import type * as NativeNavigation from '@react-navigation/native'; import {Str} from 'expensify-common'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; +import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import CONFIG from '@src/CONFIG'; @@ -14,18 +15,20 @@ import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; +console.debug = () => {}; + type MockFetch = ReturnType & { pause: () => void; fail: () => void; succeed: () => void; resume: () => Promise; - mockAPICommand: (command: string, response: OnyxResponse['onyxData']) => void; + mockAPICommand: (command: TCommand, responseHandler: (params: ApiRequestCommandParameters[TCommand]) => OnyxResponse['onyxData']) => void; }; type QueueItem = { resolve: (value: Partial | PromiseLike>) => void; input: RequestInfo; - init?: RequestInit; + options?: RequestInit; }; type FormData = { @@ -186,11 +189,12 @@ function signOutTestUser() { */ function getGlobalFetchMock(): typeof fetch { let queue: QueueItem[] = []; - let responses = new Map(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let responses = new Map OnyxResponse['onyxData']>(); let isPaused = false; let shouldFail = false; - const getResponse = (input: RequestInfo): Partial => + const getResponse = (input: RequestInfo, options?: RequestInit): Partial => shouldFail ? { ok: true, @@ -202,20 +206,22 @@ function getGlobalFetchMock(): typeof fetch { const commandMatch = typeof input === 'string' ? input.match(/https:\/\/www.expensify.com.dev\/api\/(\w+)\?/) : null; const command = commandMatch ? commandMatch[1] : null; - if (command && responses.has(command)) { - return Promise.resolve({jsonCode: 200, onyxData: responses.get(command)}); + const responseHandler = command ? responses.get(command) : null; + if (responseHandler) { + const requestData = options?.body instanceof FormData ? Object.fromEntries(options.body) : {}; + return Promise.resolve({jsonCode: 200, onyxData: responseHandler(requestData)}); } return Promise.resolve({jsonCode: 200}); }, }; - const mockFetch = jest.fn().mockImplementation((input: RequestInfo) => { + const mockFetch = jest.fn().mockImplementation((input: RequestInfo, options?: RequestInit) => { if (!isPaused) { - return Promise.resolve(getResponse(input)); + return Promise.resolve(getResponse(input, options)); } return new Promise((resolve) => { - queue.push({resolve, input}); + queue.push({resolve, input, options}); }); }) as MockFetch; @@ -237,8 +243,8 @@ function getGlobalFetchMock(): typeof fetch { }; mockFetch.fail = () => (shouldFail = true); mockFetch.succeed = () => (shouldFail = false); - mockFetch.mockAPICommand = (command: string, response: OnyxResponse['onyxData']) => { - responses.set(command, response); + mockFetch.mockAPICommand = (command: TCommand, responseHandler: (params: ApiRequestCommandParameters[TCommand]) => OnyxResponse['onyxData']): void => { + responses.set(command, responseHandler); }; return mockFetch as typeof fetch; } @@ -256,6 +262,28 @@ function setupGlobalFetchMock(): MockFetch { return mockFetch as MockFetch; } +function getFetchMockCalls(commandName: ApiCommand) { + return (global.fetch as MockFetch).mock.calls.filter((c) => c[0] === `https://www.expensify.com.dev/api/${commandName}?`); +} + +/** + * Assertion helper to validate that a command has been called a specific number of times. + */ +function expectAPICommandToHaveBeenCalled(commandName: ApiCommand, expectedCalls: number) { + expect(getFetchMockCalls(commandName)).toHaveLength(expectedCalls); +} + +/** + * Assertion helper to validate that a command has been called with specific parameters. + */ +function expectAPICommandToHaveBeenCalledWith(commandName: TCommand, callIndex: number, expectedParams: ApiRequestCommandParameters[TCommand]) { + const call = getFetchMockCalls(commandName).at(callIndex); + expect(call).toBeTruthy(); + const body = call?.at(1)?.body; + const params = body instanceof FormData ? Object.fromEntries(body) : {}; + expect(params).toEqual(expect.objectContaining(expectedParams)); +} + function setPersonalDetails(login: string, accountID: number) { Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [accountID]: buildPersonalDetails(login, accountID), @@ -292,6 +320,8 @@ export { getGlobalFetchMock, setupApp, setupGlobalFetchMock, + expectAPICommandToHaveBeenCalled, + expectAPICommandToHaveBeenCalledWith, setPersonalDetails, signInWithTestUser, signOutTestUser, diff --git a/tsconfig.json b/tsconfig.json index ea072fc4a354..16497c29b8cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "module": "commonjs", "types": ["react-native", "jest"], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "isolatedModules": true, "strict": true, "allowSyntheticDefaultImports": true, From 340176980f773372aeb341011d52219ce851d689 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 19 Jun 2024 14:48:23 -0700 Subject: [PATCH 136/512] Fix ts errors --- src/libs/actions/Policy/Policy.ts | 2 +- tests/utils/TestHelper.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index b15bcc93a6f5..dc4a7afa5d07 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2241,8 +2241,8 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string }, }, }, - ...employeeWorkspaceChat.onyxSuccessData, ]; + successData.push(...employeeWorkspaceChat.onyxSuccessData); const failureData: OnyxUpdate[] = [ { diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 81fe1e9173f4..db15bbec1965 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -312,11 +312,37 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * @returns An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners: Listener[] = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + export type {MockFetch, FormData, NativeNavigationMock}; export { assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, + createAddListenerMock, getGlobalFetchMock, setupApp, setupGlobalFetchMock, From 2bb077b0019bb8110afe1e8166e2b0c41d867282 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 19 Jun 2024 18:23:32 -0400 Subject: [PATCH 137/512] Add end marker --- src/libs/Middleware/Pagination.ts | 6 +++++- src/libs/PaginationUtils.ts | 2 +- src/libs/actions/Report.ts | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 5920dfb444b3..c518f178d31d 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -18,6 +18,7 @@ type PaginationConfig Array ? TResource : never>; getItemID: (item: OnyxValues[TResourceKey] extends Record ? TResource : never) => string; + isLastItem: (item: OnyxValues[TResourceKey] extends Record ? TResource : never) => boolean; }; type PaginationConfigMapValue = Omit, 'initialCommand' | 'previousCommand' | 'nextCommand'> & { @@ -79,7 +80,7 @@ const Pagination: Middleware = (requestResponse, request) => { return requestResponse; } - const {resourceCollectionKey, pageCollectionKey, sortItems, getItemID, type} = paginationConfig; + const {resourceCollectionKey, pageCollectionKey, sortItems, getItemID, isLastItem, type} = paginationConfig; const {resourceID, cursorID} = request; return requestResponse.then((response) => { if (!response?.onyxData) { @@ -105,6 +106,9 @@ const Pagination: Middleware = (requestResponse, request) => { if ((type === 'initial' && !cursorID) || (type === 'next' && newPage.length === 1 && newPage[0] === cursorID)) { newPage.unshift(CONST.PAGINATION_START_ID); } + if (isLastItem(sortedPageItems[sortedPageItems.length - 1])) { + newPage.push(CONST.PAGINATION_END_ID); + } const resourceCollections = resources.get(resourceCollectionKey) ?? {}; const existingItems = resourceCollections[resourceKey] ?? {}; diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 5ac438925f7d..31d765399428 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -124,7 +124,7 @@ function mergeContinuousPages(sortedItems: TResource[], pages: Pages, const prevPage = sortedPages[i - 1]; // Current page is inside the previous page, skip - if (page.lastIndex <= prevPage.lastIndex) { + if (page.lastIndex <= prevPage.lastIndex && page.lastID !== CONST.PAGINATION_END_ID) { // eslint-disable-next-line no-continue continue; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 23c278ac59cf..f18cbc0f2675 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -278,6 +278,7 @@ registerPaginationConfig({ pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), getItemID: (reportAction) => reportAction.reportActionID, + isLastItem: (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, }); function clearGroupChat() { From 093c15f4648e1c9c4100faa7b5d285ecd82d9ac2 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 19 Jun 2024 18:24:05 -0400 Subject: [PATCH 138/512] Remove console.debug mock --- tests/utils/TestHelper.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index db15bbec1965..d92c5ead477f 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -15,8 +15,6 @@ import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; -console.debug = () => {}; - type MockFetch = ReturnType & { pause: () => void; fail: () => void; From 5c521ac79cee77845d398fd23b81436907271672 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 19 Jun 2024 15:53:37 -0700 Subject: [PATCH 139/512] make storybook plugins dev dependencies --- package-lock.json | 439 ++++++++++++++++++++++++++++++++++++++++------ package.json | 10 +- 2 files changed, 393 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 292400aca6c0..b632ff5be745 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,11 +44,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.20", "@shopify/flash-list": "1.6.3", - "@storybook/addon-a11y": "^8.0.6", - "@storybook/addon-essentials": "^8.0.6", - "@storybook/cli": "^8.0.6", - "@storybook/react": "^8.0.6", - "@storybook/theming": "^8.0.6", "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -162,8 +157,13 @@ "@react-native/babel-preset": "^0.73.21", "@react-native/metro-config": "^0.73.5", "@react-navigation/devtools": "^6.0.10", + "@storybook/addon-a11y": "^8.0.6", + "@storybook/addon-essentials": "^8.0.6", "@storybook/addon-webpack5-compiler-babel": "^3.0.3", + "@storybook/cli": "^8.0.6", + "@storybook/react": "^8.0.6", "@storybook/react-webpack5": "^8.0.6", + "@storybook/theming": "^8.0.6", "@svgr/webpack": "^6.0.0", "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", @@ -377,6 +377,7 @@ "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", "integrity": "sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==", + "dev": true, "dependencies": { "default-browser-id": "3.0.0" }, @@ -2582,7 +2583,8 @@ "node_modules/@base2/pretty-print-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz", - "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==" + "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==", + "dev": true }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", @@ -2781,6 +2783,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, "optional": true, "engines": { "node": ">=0.1.90" @@ -2852,6 +2855,7 @@ }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3086,6 +3090,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "dev": true, "peerDependencies": { "react": ">=16.8.0" } @@ -3110,6 +3115,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "aix" @@ -3125,6 +3131,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -3140,6 +3147,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -3155,6 +3163,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "android" @@ -3170,6 +3179,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -3185,6 +3195,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -3200,6 +3211,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -3215,6 +3227,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -3230,6 +3243,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3245,6 +3259,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3260,6 +3275,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3275,6 +3291,7 @@ "cpu": [ "loong64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3290,6 +3307,7 @@ "cpu": [ "mips64el" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3305,6 +3323,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3320,6 +3339,7 @@ "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3335,6 +3355,7 @@ "cpu": [ "s390x" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3350,6 +3371,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -3365,6 +3387,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "netbsd" @@ -3380,6 +3403,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "openbsd" @@ -3395,6 +3419,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "sunos" @@ -3410,6 +3435,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -3425,6 +3451,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -3440,6 +3467,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -5496,7 +5524,8 @@ "node_modules/@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", - "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==" + "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", + "dev": true }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.15.0", @@ -5710,6 +5739,7 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -5725,6 +5755,7 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5735,6 +5766,7 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.1", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5745,10 +5777,12 @@ }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", + "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -5764,6 +5798,7 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -5777,6 +5812,7 @@ }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -7341,6 +7377,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz", "integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==", + "dev": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -7369,6 +7406,7 @@ "version": "3.0.9", "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", "integrity": "sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==", + "dev": true, "dependencies": { "gunzip-maybe": "^1.4.2", "pump": "^3.0.0", @@ -7648,6 +7686,7 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -7663,6 +7702,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dev": true, "dependencies": { "@babel/runtime": "^7.13.10" }, @@ -7680,6 +7720,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dev": true, "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" @@ -9439,6 +9480,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.0.6.tgz", "integrity": "sha512-p84GRmEU4f9uro71et4X4elnCFReq16UC44h8neLhcZHlMLkPop5oSRslcvF7MlKrM+mJepO1tsKmBmoTaq2PQ==", + "dev": true, "dependencies": { "@storybook/addon-highlight": "8.0.6", "axe-core": "^4.2.0" @@ -9452,6 +9494,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.0.6.tgz", "integrity": "sha512-3R/d2Td6+yeR+UnyCAeZ4tuiRGSm+6gKUQP9vB1bvEFQGuFBrV+zs3eakcYegOqZu3IXuejgaB0Knq987gUL5A==", + "dev": true, "dependencies": { "@storybook/core-events": "8.0.6", "@storybook/global": "^5.0.0", @@ -9469,6 +9512,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -9481,6 +9525,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.0.6.tgz", "integrity": "sha512-NRTmSsJiqpXqJMVrRuQ+P1wt26ZCLjBNaMafcjgicfWeyUsdhNF63yYvyrHkMRuNmYPZm0hKvtjLhW3s9VohSA==", + "dev": true, "dependencies": { "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3", @@ -9495,6 +9540,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.0.6.tgz", "integrity": "sha512-bNXDhi1xl7eat1dUsKTrUgu5mkwXjfFWDjIYxrzatqDOW1+rdkNaPFduQRJ2mpCs4cYcHKAr5chEcMm6byuTnA==", + "dev": true, "dependencies": { "@storybook/blocks": "8.0.6", "lodash": "^4.17.21", @@ -9509,6 +9555,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.0.6.tgz", "integrity": "sha512-QOlOE2XEFcUaR85YytBuf/nfKFkbIlD0Qc9CI4E65FoZPTCMhRVKAEN2CpsKI63fs/qQxM2mWkPXb6w7QXGxvg==", + "dev": true, "dependencies": { "@babel/core": "^7.12.3", "@mdx-js/react": "^3.0.0", @@ -9540,6 +9587,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -9553,6 +9601,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.0.6.tgz", "integrity": "sha512-L9SSsdN1EG2FZ1mNT59vwf0fpseLrzO1cWPwH6hVtp0+kci3tfropch2tEwO7Vr+YLSesJihfr4uvpI/l0jCsw==", + "dev": true, "dependencies": { "@storybook/addon-actions": "8.0.6", "@storybook/addon-backgrounds": "8.0.6", @@ -9578,6 +9627,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.0.6.tgz", "integrity": "sha512-CxXzzgIK5sXy2RNIkwU5JXZNq+PNGhUptRm/5M5ylcB7rk0pdwnE0TLXsMU+lzD0ji+cj61LWVLdeXQa+/whSw==", + "dev": true, "dependencies": { "@storybook/global": "^5.0.0" }, @@ -9590,6 +9640,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.0.6.tgz", "integrity": "sha512-2PnytDaQzCxcgykEM5Njb71Olm+Z2EFERL5X+5RhsG2EQxEqobwh1fUtXLY4aqiImdSJOrjQnkMJchzzoTRtug==", + "dev": true, "dependencies": { "@storybook/global": "^5.0.0", "tiny-invariant": "^1.3.1" @@ -9603,6 +9654,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.0.6.tgz", "integrity": "sha512-PfTIy64kV5h7F0tXrj5rlwdPFpOQiGrn01AQudSJDVWaMsbVgjruPU+cHG4i/L1mzzERzeHYd46bNENWZiQgDw==", + "dev": true, "dependencies": { "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" @@ -9616,6 +9668,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.0.6.tgz", "integrity": "sha512-g4GjrMEHKOIQVwG1DKUHBAn4B8xmdqlxFlVusOrYD9FVfakgMNllN6WBc02hg/IiuzqIDxVK5BXiY9MbXnoguQ==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -9625,6 +9678,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.0.6.tgz", "integrity": "sha512-R6aGEPA5e05L/NPs6Nbj0u9L6oKmchnJ/x8Rr/Xuc+nqVgXC1rslI0BcjJuC571Bewz7mT8zJ+BjP/gs7T4lnQ==", + "dev": true, "dependencies": { "memoizerific": "^1.11.3" }, @@ -9650,6 +9704,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.0.6.tgz", "integrity": "sha512-ycuPJwxyngSor4YNa4kkX3rAmX+w2pXNsIo+Zs4fEdAfCvha9+GZ/3jQSdrsHxjeIm9l9guiv4Ag8QTnnllXkw==", + "dev": true, "dependencies": { "@storybook/channels": "8.0.6", "@storybook/client-logger": "8.0.6", @@ -9697,6 +9752,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -9707,12 +9763,14 @@ "node_modules/@storybook/blocks/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@storybook/builder-manager": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-8.0.6.tgz", "integrity": "sha512-N61Gh9FKsSYvsbdBy5qFvq1anTIuUAjh2Z+ezDMlxnfMGG77nZP9heuy1NnCaYCTFzl+lq4BsmRfXXDcKtSPRA==", + "dev": true, "dependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@storybook/core-common": "8.0.6", @@ -9738,6 +9796,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -9751,6 +9810,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -9872,6 +9932,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-8.0.6.tgz", "integrity": "sha512-IbNvjxeyQKiMpb+gSpQ7yYsFqb8BM/KYgfypJM3yJV6iU/NFeevrC/DA6/R+8xWFyPc70unRNLv8fPvxhcIu8Q==", + "dev": true, "dependencies": { "@storybook/client-logger": "8.0.6", "@storybook/core-events": "8.0.6", @@ -9888,6 +9949,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-8.0.6.tgz", "integrity": "sha512-gAnl9soQUu1BtB4sANaqaaeTZAt/ThBSwCdzSLut5p21fP4ovi3FeP7hcDCJbyJZ/AvnD4k6leDrqRQxMVPr0A==", + "dev": true, "dependencies": { "@babel/core": "^7.23.0", "@babel/types": "^7.23.0", @@ -9939,6 +10001,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -9953,6 +10016,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9968,6 +10032,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -9978,12 +10043,14 @@ "node_modules/@storybook/cli/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@storybook/cli/node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", @@ -10005,6 +10072,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -10018,6 +10086,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10026,6 +10095,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10034,6 +10104,7 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", + "dev": true, "dependencies": { "@babel/core": "^7.23.0", "@babel/parser": "^7.23.0", @@ -10072,6 +10143,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10086,6 +10158,7 @@ "version": "0.23.6", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.6.tgz", "integrity": "sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==", + "dev": true, "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", @@ -10101,6 +10174,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10112,6 +10186,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dev": true, "dependencies": { "del": "^6.0.0", "is-stream": "^2.0.0", @@ -10130,6 +10205,7 @@ "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, "engines": { "node": ">=10" }, @@ -10141,6 +10217,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-8.0.6.tgz", "integrity": "sha512-et/IHPHiiOwMg93l5KSgw47NZXz5xOyIrIElRcsT1wr8OJeIB9DzopB/suoHBZ/IML+t8x91atdutzUN2BLF6A==", + "dev": true, "dependencies": { "@storybook/global": "^5.0.0" }, @@ -10153,6 +10230,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.0.6.tgz", "integrity": "sha512-IMaTVI+EvmFxkz4leKWKForPC3LFxzfeTmd/QnTNF3nCeyvmIXvP01pQXRjro0+XcGDncEStuxa1d9ClMlac9Q==", + "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", @@ -10179,6 +10257,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10193,6 +10272,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10208,6 +10288,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10218,12 +10299,14 @@ "node_modules/@storybook/codemod/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@storybook/codemod/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10232,6 +10315,7 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", + "dev": true, "dependencies": { "@babel/core": "^7.23.0", "@babel/parser": "^7.23.0", @@ -10270,6 +10354,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10284,6 +10369,7 @@ "version": "0.23.6", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.6.tgz", "integrity": "sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==", + "dev": true, "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", @@ -10299,6 +10385,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10310,6 +10397,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.0.6.tgz", "integrity": "sha512-6W2BAqAPJkrExk8D/ug2NPBPvMs05p6Bdt9tk3eWjiMrhG/CUKBzlBTEfNK/mzy3YVB6ijyT2DgsqzmWWYJ/Xw==", + "dev": true, "dependencies": { "@radix-ui/react-slot": "^1.0.2", "@storybook/client-logger": "8.0.6", @@ -10334,6 +10422,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-8.0.6.tgz", "integrity": "sha512-Z4cA52SjcW6SAV9hayqVm5kyr362O20Zmwz7+H2nYEhcu8bY69y5p45aaoyElMxL1GDNu84GrmTp7dY4URw1fQ==", + "dev": true, "dependencies": { "@storybook/core-events": "8.0.6", "@storybook/csf-tools": "8.0.6", @@ -10373,6 +10462,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10387,6 +10477,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -10395,6 +10486,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10410,6 +10502,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10420,12 +10513,14 @@ "node_modules/@storybook/core-common/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@storybook/core-common/node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", @@ -10447,6 +10542,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -10463,6 +10559,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -10475,6 +10572,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -10486,6 +10584,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -10499,6 +10598,7 @@ "version": "10.3.12", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.6", @@ -10520,6 +10620,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10528,6 +10629,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10536,6 +10638,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -10547,6 +10650,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -10561,6 +10665,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -10569,6 +10674,7 @@ "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -10583,6 +10689,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -10591,6 +10698,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -10605,6 +10713,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -10616,6 +10725,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -10624,6 +10734,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10635,6 +10746,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dev": true, "dependencies": { "del": "^6.0.0", "is-stream": "^2.0.0", @@ -10653,6 +10765,7 @@ "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, "engines": { "node": ">=10" }, @@ -10664,6 +10777,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -10676,6 +10790,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-8.0.6.tgz", "integrity": "sha512-EwGmuMm8QTUAHPhab4yftQWoSCX3OzEk6cQdpLtbNFtRRLE9aPZzxhk5Z/d3KhLNSCUAGyCiDt5I9JxTBetT9A==", + "dev": true, "dependencies": { "ts-dedent": "^2.0.0" }, @@ -10688,6 +10803,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-8.0.6.tgz", "integrity": "sha512-COmcjrry8vZXDh08ZGbfDz2bFB4of5wnwOwYf8uwlVND6HnhQzV22On1s3/p8qw+dKOpjpwDdHWtMnndnPNuqQ==", + "dev": true, "dependencies": { "@aw-web-design/x-default-browser": "1.4.126", "@babel/core": "^7.23.9", @@ -10742,6 +10858,7 @@ "version": "18.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.30.tgz", "integrity": "sha512-453z1zPuJLVDbyahaa1sSD5C2sht6ZpHp5rgJNs+H8YGqhluCXcuOUmBYsAo0Tos0cHySJ3lVUGbGgLlqIkpyg==", + "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -10750,6 +10867,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10764,6 +10882,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10779,6 +10898,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10789,12 +10909,14 @@ "node_modules/@storybook/core-server/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@storybook/core-server/node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -10808,6 +10930,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10816,6 +10939,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10827,6 +10951,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -10865,6 +10990,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.3.tgz", "integrity": "sha512-IPZvXXo4b3G+gpmgBSBqVM81jbp2ePOKsvhgJdhyZJtkYQCII7rg9KKLQhvBQM5sLaF1eU6r0iuwmyynC9d9SA==", + "dev": true, "dependencies": { "type-fest": "^2.19.0" } @@ -10873,6 +10999,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.0.6.tgz", "integrity": "sha512-ULaAFGhdgDDbknGnCqxitzeBlSzYZJQvZT4HtFgxfNU2McOu+GLIzyUOx3xG5eoziLvvm+oW+lxLr5nDkSaBUg==", + "dev": true, "dependencies": { "@storybook/csf-tools": "8.0.6", "unplugin": "^1.3.1" @@ -10886,6 +11013,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-8.0.6.tgz", "integrity": "sha512-MEBVxpnzqkBPyYXdtYQrY0SQC3oflmAQdEM0qWFzPvZXTnIMk3Q2ft8JNiBht6RlrKGvKql8TodwpbOiPeJI/w==", + "dev": true, "dependencies": { "@babel/generator": "^7.23.0", "@babel/parser": "^7.23.0", @@ -10906,6 +11034,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -10919,6 +11048,7 @@ "version": "0.23.6", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.6.tgz", "integrity": "sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==", + "dev": true, "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", @@ -10934,6 +11064,7 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, "engines": { "node": ">=12.20" }, @@ -10944,12 +11075,14 @@ "node_modules/@storybook/docs-mdx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@storybook/docs-mdx/-/docs-mdx-3.0.0.tgz", - "integrity": "sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==" + "integrity": "sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==", + "dev": true }, "node_modules/@storybook/docs-tools": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-8.0.6.tgz", "integrity": "sha512-PsAA2b/Q1ki5IR0fa52MI+fdDkQ0W+mrZVRRj3eJzonGZYcQtXofTXQB7yi0CaX7zzI/N8JcdE4bO9sI6SrOTg==", + "dev": true, "dependencies": { "@storybook/core-common": "8.0.6", "@storybook/preview-api": "8.0.6", @@ -10968,6 +11101,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", @@ -10980,6 +11114,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -10991,12 +11126,14 @@ "node_modules/@storybook/global": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==" + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true }, "node_modules/@storybook/icons": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.2.9.tgz", "integrity": "sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==", + "dev": true, "engines": { "node": ">=14.0.0" }, @@ -11009,6 +11146,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-8.0.6.tgz", "integrity": "sha512-wdL3lG72qrCOLkxEUW49+hmwA4fIFXFvAEU7wVgEt4KyRRGWhHa8Dr/5Tnq54CWJrA+BTrTPHaoo/Vu4BAjgow==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -11018,6 +11156,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.0.6.tgz", "integrity": "sha512-khYA5CM+LY/B5VsqqUmt2ivNLNqyIKfcgGsXHkOs3Kr5BOz8LhEmSwZOB348ey2C2ejFJmvKlkcsE+rB9ixlww==", + "dev": true, "dependencies": { "@storybook/channels": "8.0.6", "@storybook/client-logger": "8.0.6", @@ -11044,6 +11183,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-8.0.6.tgz", "integrity": "sha512-mDRJLVAuTWauO0mnrwajfJV/6zKBJVPp/9g0ULccE3Q+cuqNfUefqfCd17cZBlJHeRsdB9jy9tod48d4qzGEkQ==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -11148,6 +11288,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.0.6.tgz", "integrity": "sha512-O5SvBqlHIO/Cf5oGZUJV2npkp9bLqg9Sn0T0a5zXolJbRy+gP7MDyz4AnliLpTn5bT2rzVQ6VH8IDlhHBq3K6g==", + "dev": true, "dependencies": { "@storybook/channels": "8.0.6", "@storybook/client-logger": "8.0.6", @@ -11173,6 +11314,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.0.6.tgz", "integrity": "sha512-A1zivNti15nHkJ6EcVKpxKwlDkyMb5MlJMUb8chX/xBWxoR1f5R8eI484rhdPRYUzBY7JwvgZfy4y/murqg6hA==", + "dev": true, "dependencies": { "@storybook/client-logger": "8.0.6", "@storybook/docs-tools": "8.0.6", @@ -11351,6 +11493,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.0.6.tgz", "integrity": "sha512-NC4k0dBIypvVqwqnMhKDUxNc1OeL6lgspn8V26PnmCYbvY97ZqoGQ7n2a5Kw/kubN6yWX1nxNkV6HcTRgEnYTw==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -11402,6 +11545,7 @@ "version": "18.19.28", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.28.tgz", "integrity": "sha512-J5cOGD9n4x3YGgVuaND6khm5x07MMdAKkRyXnjVR6KFhLMNh2yONGiP7Z+4+tBOt5mK+GvDTiacTOVGGpqiecw==", + "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -11410,6 +11554,7 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, "engines": { "node": ">=12.20" }, @@ -11421,6 +11566,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/router/-/router-8.0.6.tgz", "integrity": "sha512-ektN0+TyQPxVxcUvt9ksGizgDM1bKFEdGJeeqv0yYaOSyC4M1e4S8QZ+Iq/p/NFNt5XJWsWU+HtQ8AzQWagQfQ==", + "dev": true, "dependencies": { "@storybook/client-logger": "8.0.6", "memoizerific": "^1.11.3", @@ -11435,6 +11581,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-8.0.6.tgz", "integrity": "sha512-kzxhhzGRSBYR4oe/Vlp/adKVxD8KWbIDMCgLWaINe14ILfEmpyrC00MXRSjS1tMF1qfrtn600Oe/xkHFQUpivQ==", + "dev": true, "dependencies": { "@storybook/client-logger": "8.0.6", "@storybook/core-common": "8.0.6", @@ -11454,6 +11601,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -11468,6 +11616,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -11483,6 +11632,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -11493,12 +11643,14 @@ "node_modules/@storybook/telemetry/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@storybook/telemetry/node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -11512,6 +11664,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -11520,6 +11673,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11531,6 +11685,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.0.6.tgz", "integrity": "sha512-o/b12+nDp8WDFlE0qQilzJ2aIeOHD48MCoc+ouFRPRH4tUS5xNaBPYxBxTgdtFbwZNuOC2my4A37Uhjn6IwkuQ==", + "dev": true, "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", "@storybook/client-logger": "8.0.6", @@ -11558,6 +11713,7 @@ "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.0.6.tgz", "integrity": "sha512-YKq4A+3diQ7UCGuyrB/9LkB29jjGoEmPl3TfV7mO1FvdRw22BNuV3GyJCiLUHigSKiZgFo+pfQhmsNRJInHUnQ==", + "dev": true, "dependencies": { "@storybook/channels": "8.0.6", "@types/express": "^4.7.0", @@ -12198,6 +12354,7 @@ }, "node_modules/@types/body-parser": { "version": "1.19.2", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -12239,6 +12396,7 @@ }, "node_modules/@types/connect": { "version": "3.4.35", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -12258,6 +12416,7 @@ "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -12273,27 +12432,32 @@ "node_modules/@types/detect-port": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", - "integrity": "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==" + "integrity": "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==", + "dev": true }, "node_modules/@types/doctrine": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.3.tgz", - "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==" + "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", + "dev": true }, "node_modules/@types/ejs": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", - "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==" + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true }, "node_modules/@types/emscripten": { "version": "1.39.10", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", - "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==" + "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", + "dev": true }, "node_modules/@types/escodegen": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", - "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==" + "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==", + "dev": true }, "node_modules/@types/eslint": { "version": "8.4.6", @@ -12319,6 +12483,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -12330,6 +12495,7 @@ "version": "4.19.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -12373,6 +12539,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, "dependencies": { "@types/unist": "*" } @@ -12397,7 +12564,8 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/http-proxy": { "version": "1.17.9", @@ -12495,6 +12663,7 @@ }, "node_modules/@types/lodash": { "version": "4.14.195", + "dev": true, "license": "MIT" }, "node_modules/@types/mapbox-gl": { @@ -12507,12 +12676,14 @@ "node_modules/@types/mdx": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.12.tgz", - "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==" + "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==", + "dev": true }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/minimatch": { "version": "3.0.5", @@ -12543,7 +12714,8 @@ "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -12563,7 +12735,8 @@ "node_modules/@types/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==" + "integrity": "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==", + "dev": true }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -12579,6 +12752,7 @@ }, "node_modules/@types/qs": { "version": "6.9.7", + "dev": true, "license": "MIT" }, "node_modules/@types/ramda": { @@ -12590,6 +12764,7 @@ }, "node_modules/@types/range-parser": { "version": "1.2.4", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -12686,12 +12861,14 @@ }, "node_modules/@types/semver": { "version": "7.5.4", + "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -12710,6 +12887,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -12741,7 +12919,8 @@ "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", + "dev": true }, "node_modules/@types/urijs": { "version": "1.19.19", @@ -12750,7 +12929,8 @@ "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true }, "node_modules/@types/verror": { "version": "1.10.9", @@ -13412,7 +13592,8 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true }, "node_modules/@urql/core": { "version": "2.3.6", @@ -13697,6 +13878,7 @@ "version": "3.0.0-rc.15", "resolved": "https://registry.npmjs.org/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.15.tgz", "integrity": "sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==", + "dev": true, "dependencies": { "tslib": "^2.4.0" }, @@ -13711,6 +13893,7 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.10.3.tgz", "integrity": "sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==", + "dev": true, "dependencies": { "@yarnpkg/libzip": "^2.3.0", "tslib": "^1.13.0" @@ -13722,12 +13905,14 @@ "node_modules/@yarnpkg/fslib/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@yarnpkg/libzip": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.3.0.tgz", "integrity": "sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==", + "dev": true, "dependencies": { "@types/emscripten": "^1.39.6", "tslib": "^1.13.0" @@ -13739,7 +13924,8 @@ "node_modules/@yarnpkg/libzip/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -13785,6 +13971,7 @@ }, "node_modules/acorn": { "version": "7.4.1", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -13837,6 +14024,7 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -13844,6 +14032,7 @@ }, "node_modules/acorn-walk": { "version": "7.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -13853,6 +14042,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "dev": true, "engines": { "node": ">= 10.0.0" } @@ -14211,7 +14401,8 @@ "node_modules/app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==" + "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", + "dev": true }, "node_modules/appdirsjs": { "version": "1.2.7", @@ -14558,6 +14749,7 @@ "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, "dependencies": { "tslib": "^2.0.1" }, @@ -14582,6 +14774,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true, "license": "MIT" }, "node_modules/async-each": { @@ -14633,6 +14826,7 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.5", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14655,6 +14849,7 @@ }, "node_modules/axe-core": { "version": "4.7.2", + "dev": true, "license": "MPL-2.0", "engines": { "node": ">=4" @@ -15520,6 +15715,7 @@ }, "node_modules/binary-extensions": { "version": "2.2.0", + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -15660,6 +15856,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, "dependencies": { "big-integer": "^1.6.44" }, @@ -15692,7 +15889,8 @@ "node_modules/browser-assert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", - "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==" + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", + "dev": true }, "node_modules/browserify-aes": { "version": "1.2.0", @@ -16352,6 +16550,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "devOptional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -16443,6 +16642,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, "dependencies": { "consola": "^3.2.3" } @@ -16608,6 +16808,7 @@ "version": "0.6.4", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", + "dev": true, "dependencies": { "string-width": "^4.2.0" }, @@ -16814,6 +17015,7 @@ }, "node_modules/commander": { "version": "6.2.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -17184,6 +17386,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, "engines": { "node": "^14.18.0 || >=16.10.0" } @@ -17998,6 +18201,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, "dependencies": { "bplist-parser": "^0.2.0", "untildify": "^4.0.0" @@ -18059,6 +18263,7 @@ }, "node_modules/define-properties": { "version": "1.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-property-descriptors": "^1.0.0", @@ -18086,7 +18291,8 @@ "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true }, "node_modules/del": { "version": "4.1.1", @@ -18238,6 +18444,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, "engines": { "node": ">=8" } @@ -18266,6 +18473,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-2.0.1.tgz", "integrity": "sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==", + "dev": true, "dependencies": { "execa": "^5.1.1" }, @@ -18277,6 +18485,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", + "dev": true, "dependencies": { "address": "^1.0.1", "debug": "4" @@ -18451,6 +18660,7 @@ }, "node_modules/doctrine": { "version": "3.0.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -18549,6 +18759,7 @@ }, "node_modules/dotenv": { "version": "16.3.1", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -18571,6 +18782,7 @@ }, "node_modules/duplexify": { "version": "3.7.1", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.0.0", @@ -18585,6 +18797,7 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", + "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -18593,6 +18806,7 @@ }, "node_modules/ejs": { "version": "3.1.9", + "dev": true, "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" @@ -19155,6 +19369,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -19191,12 +19406,14 @@ "node_modules/esbuild-plugin-alias": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", - "integrity": "sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==" + "integrity": "sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==", + "dev": true }, "node_modules/esbuild-register": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "dev": true, "dependencies": { "debug": "^4.3.4" }, @@ -21480,7 +21697,8 @@ "node_modules/fetch-retry": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", - "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==" + "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", + "dev": true }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -21497,6 +21715,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", "integrity": "sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==", + "dev": true, "dependencies": { "fs-extra": "11.1.1", "ramda": "0.29.0" @@ -21506,6 +21725,7 @@ "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -21522,6 +21742,7 @@ }, "node_modules/filelist": { "version": "1.0.4", + "dev": true, "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" @@ -21529,6 +21750,7 @@ }, "node_modules/filelist/node_modules/brace-expansion": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -21536,6 +21758,7 @@ }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -21777,6 +22000,7 @@ }, "node_modules/for-each": { "version": "0.3.3", + "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.3" @@ -21793,6 +22017,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -21808,6 +22033,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -21997,6 +22223,7 @@ }, "node_modules/fs-constants": { "version": "1.0.0", + "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -22144,6 +22371,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz", "integrity": "sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA==", + "dev": true, "engines": { "node": ">=12.17" } @@ -22213,6 +22441,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.2.3", @@ -22230,7 +22459,8 @@ "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true }, "node_modules/gl-matrix": { "version": "3.4.3", @@ -22333,6 +22563,7 @@ }, "node_modules/gopd": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" @@ -22402,6 +22633,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, "dependencies": { "browserify-zlib": "^0.1.4", "is-deflate": "^1.0.0", @@ -22418,6 +22650,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", + "dev": true, "dependencies": { "pako": "~0.2.0" } @@ -22425,7 +22658,8 @@ "node_modules/gunzip-maybe/node_modules/pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true }, "node_modules/gzip-size": { "version": "6.0.0", @@ -22450,6 +22684,7 @@ "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -22512,6 +22747,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.0", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.1" @@ -22542,6 +22778,7 @@ }, "node_modules/has-tostringtag": { "version": "1.0.0", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -22685,6 +22922,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dev": true, "dependencies": { "@types/hast": "^3.0.0" }, @@ -22697,6 +22935,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dev": true, "dependencies": { "@types/hast": "^3.0.0" }, @@ -22709,6 +22948,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "dev": true, "dependencies": { "@types/hast": "^3.0.0" }, @@ -22850,6 +23090,7 @@ }, "node_modules/html-tags": { "version": "3.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -23686,7 +23927,8 @@ "node_modules/ip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "dev": true }, "node_modules/ip-regex": { "version": "2.1.0", @@ -23706,6 +23948,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "dev": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -23726,6 +23969,7 @@ }, "node_modules/is-arguments": { "version": "1.1.1", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -23782,6 +24026,7 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "devOptional": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -23821,6 +24066,7 @@ }, "node_modules/is-callable": { "version": "1.2.7", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -23878,7 +24124,8 @@ "node_modules/is-deflate": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==" + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true }, "node_modules/is-descriptor": { "version": "1.0.2", @@ -23967,6 +24214,7 @@ }, "node_modules/is-generator-function": { "version": "1.0.10", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -23992,6 +24240,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -24076,6 +24325,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" @@ -24162,6 +24412,7 @@ }, "node_modules/is-plain-object": { "version": "5.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -24245,6 +24496,7 @@ }, "node_modules/is-typed-array": { "version": "1.1.12", + "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.11" @@ -24471,6 +24723,7 @@ }, "node_modules/jackspeak": { "version": "2.3.6", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -24487,6 +24740,7 @@ }, "node_modules/jake": { "version": "10.8.7", + "dev": true, "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", @@ -24503,6 +24757,7 @@ }, "node_modules/jake/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -24516,6 +24771,7 @@ }, "node_modules/jake/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -24530,6 +24786,7 @@ }, "node_modules/jake/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -24540,10 +24797,12 @@ }, "node_modules/jake/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/jake/node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24551,6 +24810,7 @@ }, "node_modules/jake/node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27426,6 +27686,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz", "integrity": "sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==", + "dev": true, "dependencies": { "app-root-dir": "^1.0.2", "dotenv": "^16.0.0", @@ -27439,6 +27700,7 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, "engines": { "node": ">=12" } @@ -28096,7 +28358,8 @@ "node_modules/map-or-similar": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", - "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==" + "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true }, "node_modules/map-visit": { "version": "1.0.0", @@ -28161,6 +28424,7 @@ "version": "7.3.2", "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.3.2.tgz", "integrity": "sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==", + "dev": true, "engines": { "node": ">= 10" }, @@ -28317,6 +28581,7 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, "dependencies": { "map-or-similar": "^1.5.0" } @@ -29052,7 +29317,8 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true }, "node_modules/mrmime": { "version": "1.0.1", @@ -29281,7 +29547,8 @@ "node_modules/node-fetch-native": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", - "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==" + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", @@ -29496,6 +29763,7 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", "integrity": "sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==", + "dev": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.2.3", @@ -29514,6 +29782,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", @@ -29536,6 +29805,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, "engines": { "node": ">=16" }, @@ -29547,6 +29817,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, "engines": { "node": ">=16.17.0" } @@ -29555,6 +29826,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -29566,6 +29838,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, "engines": { "node": ">=12" }, @@ -29577,6 +29850,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, "dependencies": { "path-key": "^4.0.0" }, @@ -29591,6 +29865,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, "dependencies": { "mimic-fn": "^4.0.0" }, @@ -29605,6 +29880,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, "engines": { "node": ">=12" }, @@ -29616,6 +29892,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "engines": { "node": ">=14" }, @@ -29627,6 +29904,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, "engines": { "node": ">=12" }, @@ -29740,6 +30018,7 @@ }, "node_modules/object-is": { "version": "1.1.5", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -29754,6 +30033,7 @@ }, "node_modules/object-keys": { "version": "1.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -29772,6 +30052,7 @@ }, "node_modules/object.assign": { "version": "4.1.4", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -29878,7 +30159,8 @@ "node_modules/ohash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", - "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==" + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true }, "node_modules/on-finished": { "version": "2.4.1", @@ -30486,6 +30768,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -30501,12 +30784,14 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } }, "node_modules/path-scurry/node_modules/minipass": { "version": "7.0.3", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -30535,7 +30820,8 @@ "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true }, "node_modules/pbf": { "version": "3.2.1", @@ -30578,6 +30864,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "duplexify": "^3.5.0", @@ -30640,6 +30927,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, "dependencies": { "find-up": "^5.0.0" }, @@ -30743,6 +31031,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, "dependencies": { "@babel/runtime": "^7.17.8" }, @@ -30895,6 +31184,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -31029,6 +31319,7 @@ }, "node_modules/pumpify": { "version": "1.5.1", + "dev": true, "license": "MIT", "dependencies": { "duplexify": "^3.6.0", @@ -31038,6 +31329,7 @@ }, "node_modules/pumpify/node_modules/pump": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -31332,6 +31624,7 @@ "version": "0.29.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" @@ -31454,6 +31747,7 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "dev": true, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" @@ -31563,6 +31857,7 @@ "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==", + "dev": true, "dependencies": { "@base2/pretty-print-object": "1.0.1", "is-plain-object": "5.0.0", @@ -31576,7 +31871,8 @@ "node_modules/react-element-to-jsx-string/node_modules/react-is": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==" + "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", + "dev": true }, "node_modules/react-error-boundary": { "version": "4.0.11", @@ -33101,6 +33397,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", @@ -33117,6 +33414,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -33129,6 +33427,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -33140,6 +33439,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -33154,6 +33454,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -33165,6 +33466,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -33173,6 +33475,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", @@ -33187,6 +33490,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, "engines": { "node": ">=8" } @@ -33195,6 +33499,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, "engines": { "node": ">=8" } @@ -33268,6 +33573,7 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "devOptional": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -33466,6 +33772,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dev": true, "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", @@ -33483,6 +33790,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", @@ -34784,6 +35092,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -35086,7 +35395,8 @@ "node_modules/store2": { "version": "2.14.3", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.3.tgz", - "integrity": "sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==" + "integrity": "sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==", + "dev": true }, "node_modules/storybook": { "version": "8.0.6", @@ -35133,6 +35443,7 @@ }, "node_modules/stream-shift": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/strict-uri-encode": { @@ -35175,6 +35486,7 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -35271,6 +35583,7 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -35281,6 +35594,7 @@ }, "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -35690,6 +36004,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -35700,10 +36015,12 @@ "node_modules/tar-fs/node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "node_modules/tar-stream": { "version": "2.2.0", + "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -35718,6 +36035,7 @@ }, "node_modules/tar-stream/node_modules/readable-stream": { "version": "3.6.2", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -35739,6 +36057,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==", + "dev": true, "dependencies": { "memoizerific": "^1.11.3" } @@ -36197,7 +36516,8 @@ "node_modules/tocbot": { "version": "4.25.0", "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.25.0.tgz", - "integrity": "sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==" + "integrity": "sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==", + "dev": true }, "node_modules/toidentifier": { "version": "1.0.1", @@ -36274,6 +36594,7 @@ }, "node_modules/ts-dedent": { "version": "2.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -36627,12 +36948,14 @@ "node_modules/ufo": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==" + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "dev": true }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -36748,6 +37071,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -36760,6 +37084,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", @@ -36774,6 +37099,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" @@ -36806,6 +37132,7 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.10.1.tgz", "integrity": "sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==", + "dev": true, "dependencies": { "acorn": "^8.11.3", "chokidar": "^3.6.0", @@ -36820,6 +37147,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -36831,6 +37159,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, "engines": { "node": ">=10.13.0" } @@ -36838,7 +37167,8 @@ "node_modules/unplugin/node_modules/webpack-virtual-modules": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz", - "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==" + "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==", + "dev": true }, "node_modules/unset-value": { "version": "1.0.0", @@ -36893,6 +37223,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, "engines": { "node": ">=8" } @@ -38206,6 +38537,7 @@ }, "node_modules/which-typed-array": { "version": "1.1.11", + "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", @@ -38256,7 +38588,8 @@ "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -38276,6 +38609,7 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -38291,6 +38625,7 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -38304,6 +38639,7 @@ }, "node_modules/wrap-ansi-cjs/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -38314,6 +38650,7 @@ }, "node_modules/wrap-ansi-cjs/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/ansi-styles": { diff --git a/package.json b/package.json index 00826a50a622..f6368489d5a7 100644 --- a/package.json +++ b/package.json @@ -96,11 +96,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.20", "@shopify/flash-list": "1.6.3", - "@storybook/addon-a11y": "^8.0.6", - "@storybook/addon-essentials": "^8.0.6", - "@storybook/cli": "^8.0.6", - "@storybook/react": "^8.0.6", - "@storybook/theming": "^8.0.6", "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -214,8 +209,13 @@ "@react-native/babel-preset": "^0.73.21", "@react-native/metro-config": "^0.73.5", "@react-navigation/devtools": "^6.0.10", + "@storybook/addon-a11y": "^8.0.6", + "@storybook/addon-essentials": "^8.0.6", "@storybook/addon-webpack5-compiler-babel": "^3.0.3", + "@storybook/cli": "^8.0.6", + "@storybook/react": "^8.0.6", "@storybook/react-webpack5": "^8.0.6", + "@storybook/theming": "^8.0.6", "@svgr/webpack": "^6.0.0", "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", From 60f3798396ac4485943ace7c5d395b476d66a3dd Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 19 Jun 2024 16:41:50 -0700 Subject: [PATCH 140/512] Fix storybook --- __mocks__/@react-navigation/native/index.ts | 49 ++++++++++----------- src/libs/Network/enhanceParameters.ts | 4 +- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 747b6761fd6d..4e248d245988 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,27 +1,29 @@ -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -const realReactNavigation = jest.requireActual('@react-navigation/native'); +const isJestEnv = process.env.NODE_ENV === 'test'; -// We only want these mocked for storybook, not jest -const useIsFocused = process.env.NODE_ENV === 'test' ? realReactNavigation.useIsFocused : () => true; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +const realReactNavigation = isJestEnv ? jest.requireActual('@react-navigation/native') : require('@react-navigation/native'); -const useTheme = process.env.NODE_ENV === 'test' ? realReactNavigation.useTheme : () => ({}); +const useIsFocused = isJestEnv ? realReactNavigation.useIsFocused : () => true; +const useTheme = isJestEnv ? realReactNavigation.useTheme : () => ({}); type Listener = () => void; - const transitionEndListeners: Listener[] = []; - -const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); -}; - -const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; -}); +const triggerTransitionEnd = isJestEnv + ? () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + } + : realReactNavigation.triggerTransitionEnd; + +const addListener = isJestEnv + ? jest.fn().mockImplementation((listener, callback: Listener) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }) + : realReactNavigation.addListener; const useNavigation = () => ({ navigate: jest.fn(), @@ -32,10 +34,5 @@ const useNavigation = () => ({ addListener, }); -module.exports = { - ...realReactNavigation, - useIsFocused, - useTheme, - useNavigation, - triggerTransitionEnd, -}; +export * from '@react-navigation/core'; +export {useIsFocused, useTheme, useNavigation, triggerTransitionEnd}; diff --git a/src/libs/Network/enhanceParameters.ts b/src/libs/Network/enhanceParameters.ts index ea7f929a421a..9a45b801b0ed 100644 --- a/src/libs/Network/enhanceParameters.ts +++ b/src/libs/Network/enhanceParameters.ts @@ -1,7 +1,7 @@ import * as Environment from '@libs/Environment/Environment'; import getPlatform from '@libs/getPlatform'; import CONFIG from '@src/CONFIG'; -import {version as pkgVersion} from '../../../package.json'; +import pkg from '../../../package.json'; import * as NetworkStore from './NetworkStore'; /** @@ -37,7 +37,7 @@ export default function enhanceParameters(command: string, parameters: Record Date: Wed, 19 Jun 2024 16:47:06 -0700 Subject: [PATCH 141/512] Update Podfile.lock --- ios/Podfile.lock | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 077003ed5285..ddab159714fc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1243,13 +1243,7 @@ PODS: - react-native-config (1.5.0): - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React - - React-Codegen - - React-RCTFabric - - ReactCommon/turbomodule/core + - React-Core - react-native-document-picker (9.1.1): - RCT-Folly - RCTRequired @@ -2558,7 +2552,7 @@ SPEC CHECKSUMS: react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 - react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c + react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 @@ -2629,8 +2623,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 From 2f3a152312dea599e835af81bf28b54c7602821b Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 19 Jun 2024 16:56:44 -0700 Subject: [PATCH 142/512] DRY up listener mock --- __mocks__/@react-navigation/native/index.ts | 26 +++++++-------------- tests/utils/TestHelper.ts | 2 ++ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 4e248d245988..35292737e374 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,3 +1,5 @@ +import {createAddListenerMock} from '../../../tests/utils/TestHelper'; + const isJestEnv = process.env.NODE_ENV === 'test'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports @@ -6,24 +8,12 @@ const realReactNavigation = isJestEnv ? jest.requireActual true; const useTheme = isJestEnv ? realReactNavigation.useTheme : () => ({}); -type Listener = () => void; -const transitionEndListeners: Listener[] = []; -const triggerTransitionEnd = isJestEnv - ? () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - } - : realReactNavigation.triggerTransitionEnd; - -const addListener = isJestEnv - ? jest.fn().mockImplementation((listener, callback: Listener) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }) - : realReactNavigation.addListener; +const {triggerTransitionEnd, addListener} = isJestEnv + ? createAddListenerMock() + : { + triggerTransitionEnd: realReactNavigation.triggerTransitionEnd, + addListener: realReactNavigation.addListener, + }; const useNavigation = () => ({ navigate: jest.fn(), diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index d92c5ead477f..9db21b7524a8 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -310,6 +310,8 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } +type Listener = () => void; + /** * This is a helper function to create a mock for the addListener function of the react-navigation library. * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate From 14442388e2bd2ae3144eb7fe068e270c96c6f27c Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 19 Jun 2024 18:04:15 -0700 Subject: [PATCH 143/512] Fix tsc and lint --- __mocks__/@react-navigation/native/index.ts | 10 +++++----- tests/unit/MigrationTest.ts | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 35292737e374..f08bb22f5944 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,9 +1,9 @@ +import type * as ReactNavigation from '@react-navigation/native'; import {createAddListenerMock} from '../../../tests/utils/TestHelper'; const isJestEnv = process.env.NODE_ENV === 'test'; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -const realReactNavigation = isJestEnv ? jest.requireActual('@react-navigation/native') : require('@react-navigation/native'); +const realReactNavigation = isJestEnv ? jest.requireActual('@react-navigation/native') : (require('@react-navigation/native') as typeof ReactNavigation); const useIsFocused = isJestEnv ? realReactNavigation.useIsFocused : () => true; const useTheme = isJestEnv ? realReactNavigation.useTheme : () => ({}); @@ -11,13 +11,13 @@ const useTheme = isJestEnv ? realReactNavigation.useTheme : () => ({}); const {triggerTransitionEnd, addListener} = isJestEnv ? createAddListenerMock() : { - triggerTransitionEnd: realReactNavigation.triggerTransitionEnd, - addListener: realReactNavigation.addListener, + triggerTransitionEnd: () => {}, + addListener: () => {}, }; const useNavigation = () => ({ - navigate: jest.fn(), ...realReactNavigation.useNavigation, + navigate: jest.fn(), getState: () => ({ routes: [], }), diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index 4eeaec777459..e7352743627a 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; import type {OnyxInputValue} from 'react-native-onyx'; -import CONST from '@src/CONST'; import Log from '@src/libs/Log'; import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; import ONYXKEYS from '@src/ONYXKEYS'; From 8099d789bba5efe7b2533d92ac63c2240eebf7ed Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 20 Jun 2024 14:43:02 +0800 Subject: [PATCH 144/512] don't set initial focus for sign in page --- .../FocusTrap/FocusTrapForScreen/index.web.tsx | 10 +++++++--- src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts | 10 +++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index 6a1409ab4a93..5e41e64b3115 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -1,12 +1,14 @@ import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; import FocusTrap from 'focus-trap-react'; import React, {useCallback, useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; -import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; +import getScreenWithAutofocus from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import ONYXKEYS from '@src/ONYXKEYS'; import type FocusTrapProps from './FocusTrapProps'; let activeRouteName = ''; @@ -14,6 +16,8 @@ function FocusTrapForScreen({children}: FocusTrapProps) { const isFocused = useIsFocused(); const route = useRoute(); const {isSmallScreenWidth} = useWindowDimensions(); + const [isAuthenticated] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => !!session?.authToken}); + const screensWithAutofocus = useMemo(() => getScreenWithAutofocus(isAuthenticated), [isAuthenticated]); const isActive = useMemo(() => { // Focus trap can't be active on bottom tab screens because it would block access to the tab bar. @@ -49,13 +53,13 @@ function FocusTrapForScreen({children}: FocusTrapProps) { fallbackFocus: document.body, // We don't want to ovverride autofocus on these screens. initialFocus: () => { - if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + if (screensWithAutofocus.includes(activeRouteName)) { return false; } return undefined; }, setReturnFocus: (element) => { - if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + if (screensWithAutofocus.includes(activeRouteName)) { return false; } return element; diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts index ec2977ea592c..7af327d35ac4 100644 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -1,4 +1,5 @@ import {CENTRAL_PANE_WORKSPACE_SCREENS} from '@libs/Navigation/AppNavigator/Navigators/FullScreenNavigator'; +import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; const SCREENS_WITH_AUTOFOCUS: string[] = [ @@ -13,4 +14,11 @@ const SCREENS_WITH_AUTOFOCUS: string[] = [ SCREENS.SIGN_IN_ROOT, ]; -export default SCREENS_WITH_AUTOFOCUS; +function getScreenWithAutofocus(isAuthenticated: boolean) { + if (!isAuthenticated) { + return [...SCREENS_WITH_AUTOFOCUS, NAVIGATORS.BOTTOM_TAB_NAVIGATOR]; + } + return SCREENS_WITH_AUTOFOCUS; +} + +export default getScreenWithAutofocus; From e30384394140cb717c20d039f66a6defac747996 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 20 Jun 2024 12:02:03 +0200 Subject: [PATCH 145/512] Update maxDelay for doubleTapGesture for mobile browsers --- src/components/MultiGestureCanvas/useTapGestures.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index f550e93d6be2..9036ae7ae39a 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,6 +3,7 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as Browser from '@libs/Browser'; import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -129,7 +130,7 @@ const useTapGestures = ({ state.fail(); }) .numberOfTaps(2) - .maxDelay(150) + .maxDelay(Browser.isMobile() ? 300 : 150) .maxDistance(20) .onEnd((evt) => { const triggerScaleChangedEvent = () => { From e07a5ce25bd7bdf63b512a3fcbe51d5dd75b8e7b Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 20 Jun 2024 13:32:56 +0200 Subject: [PATCH 146/512] filtering report actions out added --- src/libs/ExportOnyxState/common.ts | 27 ++++++++++++++---------- src/libs/ExportOnyxState/index.native.ts | 8 +++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index 09b2d955400e..b152ce89e3f4 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -1,20 +1,25 @@ import {Str} from 'expensify-common'; +import ONYXKEYS from '@src/ONYXKEYS'; const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const value = data[key]; - if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { - maskedData[key] = '***'; - } else if (typeof value === 'object') { - maskedData[key] = maskFragileData(value as Record); - } else { - maskedData[key] = value; - } + const keys = Object.keys(data).filter((key) => !key.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS)); + keys.forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + return; } - } + + const value = data[key]; + + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record); + } else { + maskedData[key] = value; + } + }); return maskedData; }; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index ff8ff9e4f730..501a219f03bc 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -25,11 +25,9 @@ const shareAsFile = (value: string) => { const actualInfoFile = `file://${infoFilePath}`; RNFS.writeFile(infoFilePath, value, 'utf8').then(() => { - const shareOptions = { - urls: [actualInfoFile], - }; - - Share.open(shareOptions); + Share.open({ + url: actualInfoFile, + }); }); } catch (error) { console.error('Error renaming and sharing file:', error); From ee0f65bce82043d41855d65b58e6385f2b68d15c Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:28:02 +0200 Subject: [PATCH 147/512] add trip receipt case --- src/components/EReceiptThumbnail.tsx | 12 ++++++++- .../ReportActionItemImage.tsx | 12 +++++++++ src/libs/TripReservationUtils.ts | 25 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index f4216dcc9f8a..6762538b3c91 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -9,6 +9,7 @@ import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; @@ -56,7 +57,8 @@ const backgroundImages = { function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); + const {tripIcon, tripBGColor} = TripReservationUtils.getTripEReceiptData(transaction); + const colorCode = tripBGColor ?? (isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction)); const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); @@ -141,6 +143,14 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT fill={primaryColor} /> ) : null} + {tripIcon && isReceiptThumbnail ? ( + + ) : null} diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 1251be83994b..b0105039bc18 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -95,6 +95,8 @@ function ReportActionItemImage({ const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); + const shouldUseTripEReceiptThumbnail = transaction?.receipt?.reservationList?.length !== 0; + let propsObj: ReceiptImageProps; if (isEReceipt) { @@ -110,6 +112,16 @@ function ReportActionItemImage({ }; } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { propsObj = {isPDFThumbnail: true, source: attachmentModalSource}; + } else if (shouldUseTripEReceiptThumbnail) { + propsObj = { + isThumbnail, + transactionID: transaction?.transactionID, + ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}), + shouldUseThumbnailImage: true, + isAuthTokenRequired: false, + source: thumbnail ?? image ?? '', + shouldUseInitialObjectPosition: isDistanceRequest, + }; } else { propsObj = { isThumbnail, diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index ead786b8eafd..ced50c7ec0ac 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -3,6 +3,7 @@ import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; +import { EReceiptColorName } from '@styles/utils/types'; function getTripReservationIcon(reservationType: ReservationType): IconAsset { switch (reservationType) { @@ -24,4 +25,26 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser .flat(); } -export {getTripReservationIcon, getReservationsFromTripTransactions}; +type TripEReceiptData = { + /** Icon asset associated with the type of trip reservation */ + tripIcon?: IconAsset, + + /** EReceipt background color associated with the type of trip reservation */ + tripBGColor?: EReceiptColorName, +} + +function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { + const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : ''; + + switch (reservationType) { + case CONST.RESERVATION_TYPE.FLIGHT: + case CONST.RESERVATION_TYPE.CAR: + return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK}; + case CONST.RESERVATION_TYPE.HOTEL: + return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; + default: + return {}; + } +} + +export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptData}; From 4e34eb15f41366c75698a3eb0567ae508d686bcd Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:29:11 +0200 Subject: [PATCH 148/512] fix prettier --- src/components/EReceiptThumbnail.tsx | 2 +- src/libs/TripReservationUtils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 6762538b3c91..aebfff1f5a00 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -5,11 +5,11 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as TripReservationUtils from '@libs/TripReservationUtils'; import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index ced50c7ec0ac..23a1743d65f4 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -1,9 +1,9 @@ +import {EReceiptColorName} from '@styles/utils/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; -import { EReceiptColorName } from '@styles/utils/types'; function getTripReservationIcon(reservationType: ReservationType): IconAsset { switch (reservationType) { @@ -27,11 +27,11 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser type TripEReceiptData = { /** Icon asset associated with the type of trip reservation */ - tripIcon?: IconAsset, + tripIcon?: IconAsset; /** EReceipt background color associated with the type of trip reservation */ - tripBGColor?: EReceiptColorName, -} + tripBGColor?: EReceiptColorName; +}; function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : ''; From 44a368f7cd09575073433837bee635a0dd2cf833 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:04:47 +0200 Subject: [PATCH 149/512] correct type imports and lint --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 23a1743d65f4..140347a99ed7 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -1,4 +1,4 @@ -import {EReceiptColorName} from '@styles/utils/types'; +import type {EReceiptColorName} from '@styles/utils/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; From 85f4941b0c71952a64e352ec9fbe60a1ad1419d2 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:05:53 +0200 Subject: [PATCH 150/512] set correct icon for accommodation --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 140347a99ed7..e937979ae7b9 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -41,7 +41,7 @@ function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { case CONST.RESERVATION_TYPE.CAR: return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK}; case CONST.RESERVATION_TYPE.HOTEL: - return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; + return {tripIcon: Expensicons.Bed, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; default: return {}; } From 267719f7100ff318717669dacb6dd7f62deb3240 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 20 Jun 2024 17:55:05 +0200 Subject: [PATCH 151/512] Fix child props --- src/components/OfflineWithFeedback.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index c12e73280c7d..cc7f6770d87b 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -1,6 +1,6 @@ import {mapValues} from 'lodash'; import React, {useCallback} from 'react'; -import type {ImageStyle, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -59,7 +59,7 @@ type OfflineWithFeedbackProps = ChildrenProps & { canDismissError?: boolean; }; -type StrikethroughProps = Partial & {style: Array}; +type StrikethroughProps = Partial & {style: AllStyles[]}; function OfflineWithFeedback({ pendingAction, @@ -106,9 +106,9 @@ function OfflineWithFeedback({ return child; } - const childProps = child.props as {children: React.ReactNode | undefined; style: AllStyles}; + const childProps = child.props as {children?: React.ReactNode; style?: AllStyles}; const props: StrikethroughProps = { - style: StyleUtils.combineStyles(childProps.style, styles.offlineFeedback.deleted, styles.userSelectNone), + style: StyleUtils.combineStyles(childProps.style ?? [], styles.offlineFeedback.deleted, styles.userSelectNone), }; if (childProps.children) { From 206cc3d3db57e60230827a9d8542d7706d31bd84 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 20 Jun 2024 18:28:44 +0200 Subject: [PATCH 152/512] Rerun lint From 9e768afffea2469a2dfde03de289c6a1b49145b1 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Jun 2024 12:38:26 -0700 Subject: [PATCH 153/512] Fix test ID (updated on main) --- tests/ui/PaginationTest.tsx | 2 +- tests/utils/debug.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 794c54d4a121..f5a44259196a 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -49,7 +49,7 @@ function scrollToOffset(offset: number) { } function triggerListLayout() { - fireEvent(screen.getByTestId('report-actions-view-container'), 'onLayout', { + fireEvent(screen.getByTestId('report-actions-view-wrapper'), 'onLayout', { nativeEvent: { layout: { x: 0, diff --git a/tests/utils/debug.ts b/tests/utils/debug.ts index ba27e9725b88..0b196417295c 100644 --- a/tests/utils/debug.ts +++ b/tests/utils/debug.ts @@ -100,7 +100,7 @@ function formatNode(node: ReactTestInstance, options: Options) { /** * Log a subtree of the app for debugging purposes. * - * @example debug(screen.getByTestId('report-actions-view-container')); + * @example debug(screen.getByTestId('report-actions-view-wrapper')); */ export default function debug(node: ReactTestInstance | ReactTestInstance[] | null, {includeProps = true, maxDepth = Infinity}: Options = {}): void { const options = {includeProps, maxDepth}; From c8e3dbe12b06b6a636348ad264fbd8759ecb17ab Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Jun 2024 13:44:22 -0700 Subject: [PATCH 154/512] WTF - why does this fix my test? --- tests/utils/TestHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 9db21b7524a8..7862c1b07be2 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -319,7 +319,7 @@ type Listener = () => void; * * @returns An object with two functions: triggerTransitionEnd and addListener */ -const createAddListenerMock = () => { +function createAddListenerMock() { const transitionEndListeners: Listener[] = []; const triggerTransitionEnd = () => { transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); @@ -335,7 +335,7 @@ const createAddListenerMock = () => { }); return {triggerTransitionEnd, addListener}; -}; +} export type {MockFetch, FormData, NativeNavigationMock}; export { From 29fcb8bd53b960c14ec08a4140eead3345205430 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Jun 2024 13:54:01 -0700 Subject: [PATCH 155/512] Use correct type in processRequest for side effect requests --- src/libs/API/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 16510b43dba3..b413a939b9ee 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -167,7 +167,7 @@ function makeRequestWithSideEffects( const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData); // Return a promise containing the response from HTTPS - return processRequest(request, CONST.API_REQUEST_TYPE.WRITE); + return processRequest(request, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS); } /** From 428ed080036f17dfa11eba18d6a6ed91fdf85f62 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 20 Jun 2024 16:56:59 -0400 Subject: [PATCH 156/512] Fix and add tests --- __mocks__/@react-navigation/native/index.ts | 18 +- src/pages/home/ReportScreen.tsx | 2 +- tests/perf-test/ChatFinderPage.perf-test.tsx | 5 +- tests/perf-test/ReportScreen.perf-test.tsx | 3 +- tests/ui/PaginationTest.tsx | 202 +++++++++++++------ tests/ui/UnreadIndicatorsTest.tsx | 11 +- tests/utils/TestHelper.ts | 35 +--- tests/utils/createAddListenerMock.ts | 28 +++ 8 files changed, 195 insertions(+), 109 deletions(-) create mode 100644 tests/utils/createAddListenerMock.ts diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index f08bb22f5944..26b7e0eb6181 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,5 +1,6 @@ +/* eslint-disable import/prefer-default-export, import/no-import-module-exports */ import type * as ReactNavigation from '@react-navigation/native'; -import {createAddListenerMock} from '../../../tests/utils/TestHelper'; +import createAddListenerMock from '../../../tests/utils/createAddListenerMock'; const isJestEnv = process.env.NODE_ENV === 'test'; @@ -24,5 +25,16 @@ const useNavigation = () => ({ addListener, }); -export * from '@react-navigation/core'; -export {useIsFocused, useTheme, useNavigation, triggerTransitionEnd}; +type NativeNavigationMock = typeof ReactNavigation & { + triggerTransitionEnd: () => void; +}; + +module.exports = { + ...realReactNavigation, + useIsFocused, + useTheme, + useNavigation, + triggerTransitionEnd, +}; + +export type {NativeNavigationMock}; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index fd0a57dd9b19..a8513911e46a 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -666,7 +666,7 @@ function ReportScreen({ navigation={navigation} style={screenWrapperStyle} shouldEnableKeyboardAvoidingView={isTopMostReportId || isReportOpenInRHP} - testID={ReportScreen.displayName} + testID={`report-screen-${report.reportID}`} > { - const {addListener} = TestHelper.createAddListenerMock(); + const {addListener} = createAddListenerMock(); const scenario = async () => { await screen.findByTestId('ChatFinderPage'); @@ -197,7 +198,7 @@ test('[ChatFinderPage] should render list with cached options', async () => { }); test('[ChatFinderPage] should interact when text input changes', async () => { - const {addListener} = TestHelper.createAddListenerMock(); + const {addListener} = createAddListenerMock(); const scenario = async () => { await screen.findByTestId('ChatFinderPage'); diff --git a/tests/perf-test/ReportScreen.perf-test.tsx b/tests/perf-test/ReportScreen.perf-test.tsx index d452e9412655..30b73f591b0f 100644 --- a/tests/perf-test/ReportScreen.perf-test.tsx +++ b/tests/perf-test/ReportScreen.perf-test.tsx @@ -27,6 +27,7 @@ import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReport from '../utils/collections/reports'; +import createAddListenerMock from '../utils/createAddListenerMock'; import PusherHelper from '../utils/PusherHelper'; import * as ReportTestUtils from '../utils/ReportTestUtils'; import * as TestHelper from '../utils/TestHelper'; @@ -168,7 +169,7 @@ const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); const mockRoute = {params: {reportID: '1', reportActionID: ''}, key: 'Report', name: 'Report' as const}; test.skip('[ReportScreen] should render ReportScreen', () => { - const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); + const {triggerTransitionEnd, addListener} = createAddListenerMock(); const scenario = async () => { /** * First make sure ReportScreen is mounted, so that we can trigger diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index f5a44259196a..8c78ad13ec4c 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -1,15 +1,17 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as NativeNavigation from '@react-navigation/native'; -import {act, fireEvent, render, screen} from '@testing-library/react-native'; +import {act, fireEvent, render, screen, within} from '@testing-library/react-native'; import {addSeconds, format, subMinutes} from 'date-fns'; import React from 'react'; import Onyx from 'react-native-onyx'; import * as Localize from '@libs/Localize'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; import * as AppActions from '@userActions/App'; import * as User from '@userActions/User'; import App from '@src/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {NativeNavigationMock} from '../../__mocks__/@react-navigation/native'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -34,6 +36,18 @@ const LIST_CONTENT_SIZE = { width: 300, height: 600, }; +const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + +const REPORT_ID = '1'; +const COMMENT_LINKING_REPORT_ID = '2'; +const USER_A_ACCOUNT_ID = 1; +const USER_A_EMAIL = 'user_a@test.com'; +const USER_B_ACCOUNT_ID = 2; +const USER_B_EMAIL = 'user_b@test.com'; + +function getReportScreen(reportID = REPORT_ID) { + return screen.getByTestId(`report-screen-${reportID}`); +} function scrollToOffset(offset: number) { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); @@ -48,8 +62,9 @@ function scrollToOffset(offset: number) { }); } -function triggerListLayout() { - fireEvent(screen.getByTestId('report-actions-view-wrapper'), 'onLayout', { +function triggerListLayout(reportID?: string) { + const report = getReportScreen(reportID); + fireEvent(within(report).getByTestId('report-actions-view-wrapper'), 'onLayout', { nativeEvent: { layout: { x: 0, @@ -58,94 +73,99 @@ function triggerListLayout() { }, }, }); - fireEvent(screen.getByTestId('report-actions-list'), 'onContentSizeChange', LIST_CONTENT_SIZE.width, LIST_CONTENT_SIZE.height); + + fireEvent(within(report).getByTestId('report-actions-list'), 'onContentSizeChange', LIST_CONTENT_SIZE.width, LIST_CONTENT_SIZE.height); } -function getReportActions() { +function getReportActions(reportID?: string) { + const report = getReportScreen(reportID); return [ - ...screen.queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatMessage')), + ...within(report).queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatMessage')), // Created action has a different accessibility label. - ...screen.queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatWelcomeMessage')), + ...within(report).queryAllByLabelText(Localize.translateLocal('accessibilityHints.chatWelcomeMessage')), ]; } -async function navigateToSidebarOption(index: number): Promise { - const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); - const optionRows = screen.queryAllByAccessibilityHint(hintText); - fireEvent(optionRows[index], 'press'); +async function navigateToSidebarOption(reportID: string): Promise { + const optionRow = screen.getByTestId(reportID); + fireEvent(optionRow, 'press'); await act(() => { - (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd(); + (NativeNavigation as NativeNavigationMock).triggerTransitionEnd(); }); // ReportScreen relies on the onLayout event to receive updates from onyx. - triggerListLayout(); + triggerListLayout(reportID); await waitForBatchedUpdatesWithAct(); } -const REPORT_ID = '1'; -const USER_A_ACCOUNT_ID = 1; -const USER_A_EMAIL = 'user_a@test.com'; -const USER_B_ACCOUNT_ID = 2; -const USER_B_EMAIL = 'user_b@test.com'; +function buildCreatedAction(reportActionID: string, created: string) { + return { + reportActionID, + actionName: 'CREATED' as const, + created, + message: [ + { + type: 'TEXT', + text: 'CREATED', + }, + ], + }; +} function buildReportComments(count: number, initialID: string, reverse = false) { let currentID = parseInt(initialID, 10); - const TEN_MINUTES_AGO = subMinutes(new Date(), 10); return Object.fromEntries( Array.from({length: Math.min(count, currentID)}).map(() => { const created = format(addSeconds(TEN_MINUTES_AGO, 10 * currentID), CONST.DATE.FNS_DB_FORMAT_STRING); const id = currentID; currentID += reverse ? 1 : -1; - return [ - `${id}`, - id === 1 - ? { - reportActionID: '1', - actionName: 'CREATED' as const, - created, - message: [ - { - type: 'TEXT', - text: 'CREATED', - }, - ], - } - : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${id}`), - ]; + return [`${id}`, id === 1 ? buildCreatedAction('1', created) : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${id}`)]; }), ); } function mockOpenReport(messageCount: number, initialID: string) { - fetchMock.mockAPICommand('OpenReport', () => [ - { - onyxMethod: 'merge', - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - value: buildReportComments(messageCount, initialID), - }, - ]); + fetchMock.mockAPICommand('OpenReport', ({reportID}) => + reportID === REPORT_ID + ? [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + value: buildReportComments(messageCount, initialID), + }, + ] + : [], + ); } function mockGetOlderActions(messageCount: number) { - fetchMock.mockAPICommand('GetOlderActions', ({reportActionID}) => [ - { - onyxMethod: 'merge', - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - // The API also returns the action that was requested with the reportActionID. - value: buildReportComments(messageCount + 1, reportActionID), - }, - ]); + fetchMock.mockAPICommand('GetOlderActions', ({reportID, reportActionID}) => + reportID === REPORT_ID + ? [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + // The API also returns the action that was requested with the reportActionID. + value: buildReportComments(messageCount + 1, reportActionID), + }, + ] + : [], + ); } -// function mockGetNewerActions(messageCount: number) { -// fetchMock.mockAPICommand('GetNewerActions', ({reportActionID}) => [ -// { -// onyxMethod: 'merge', -// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, -// // The API also returns the action that was requested with the reportActionID. -// value: buildReportComments(messageCount + 1, reportActionID, true), -// }, -// ]); -// } +function mockGetNewerActions(messageCount: number) { + fetchMock.mockAPICommand('GetNewerActions', ({reportID, reportActionID}) => + reportID === REPORT_ID + ? [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + // The API also returns the action that was requested with the reportActionID. + value: buildReportComments(messageCount + 1, reportActionID, true), + }, + ] + : [], + ); +} /** * Sets up a test with a logged in user. Returns the test instance. @@ -155,7 +175,7 @@ async function signInAndGetApp(): Promise { render(); await waitForBatchedUpdatesWithAct(); const hintText = Localize.translateLocal('loginForm.loginForm'); - const loginForm = screen.queryAllByLabelText(hintText); + const loginForm = await screen.findAllByLabelText(hintText); expect(loginForm).toHaveLength(1); await act(async () => { @@ -183,6 +203,34 @@ async function signInAndGetApp(): Promise { [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), }); + // Setup a 2nd report to test comment linking. + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${COMMENT_LINKING_REPORT_ID}`, { + reportID: COMMENT_LINKING_REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastMessageText: 'Test', + participants: {[USER_A_ACCOUNT_ID]: {hidden: false}}, + lastActorAccountID: USER_A_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${COMMENT_LINKING_REPORT_ID}`, { + '100': buildCreatedAction('100', format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING)), + '101': { + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + person: [{type: 'TEXT', style: 'strong', text: 'User B'}], + created: format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), + message: [ + { + type: 'COMMENT', + html: 'Link 1', + text: 'Link 1', + }, + ], + reportActionID: '101', + actorAccountID: USER_A_ACCOUNT_ID, + }, + }); + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests AppActions.setSidebarLoaded(); }); @@ -192,6 +240,7 @@ async function signInAndGetApp(): Promise { describe('Pagination', () => { afterEach(async () => { + await SequentialQueue.waitForIdle(); await act(async () => { await Onyx.clear(); @@ -208,7 +257,7 @@ describe('Pagination', () => { mockOpenReport(5, '5'); await signInAndGetApp(); - await navigateToSidebarOption(0); + await navigateToSidebarOption(REPORT_ID); expect(getReportActions()).toHaveLength(5); TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 1); @@ -232,7 +281,7 @@ describe('Pagination', () => { mockGetOlderActions(5); await signInAndGetApp(); - await navigateToSidebarOption(0); + await navigateToSidebarOption(REPORT_ID); expect(getReportActions()).toHaveLength(5); TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 1); @@ -248,5 +297,32 @@ describe('Pagination', () => { TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 1); TestHelper.expectAPICommandToHaveBeenCalledWith('GetOlderActions', 0, {reportID: REPORT_ID, reportActionID: '4'}); TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + + await waitForBatchedUpdatesWithAct(); + + expect(getReportActions()).toHaveLength(8); + }); + + it('opens a chat and load newer messages', async () => { + mockOpenReport(5, '5'); + mockGetOlderActions(5); + mockGetNewerActions(5); + + await signInAndGetApp(); + await navigateToSidebarOption(COMMENT_LINKING_REPORT_ID); + + const link = screen.getByText('Link 1'); + fireEvent(link, 'press'); + await act(() => { + (NativeNavigation as NativeNavigationMock).triggerTransitionEnd(); + }); + // ReportScreen relies on the onLayout event to receive updates from onyx. + triggerListLayout(); + + TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 2); + TestHelper.expectAPICommandToHaveBeenCalledWith('OpenReport', 1, {reportID: REPORT_ID, reportActionID: '5'}); + TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); + expect(getReportActions()).toHaveLength(5); }); }); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 3f24df2c24c7..9299170a9d78 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -21,6 +21,7 @@ import App from '@src/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction, ReportActions} from '@src/types/onyx'; +import type {NativeNavigationMock} from '../../__mocks__/@react-navigation/native'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -216,7 +217,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(async () => { - await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); + await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); // That the report actions are visible along with the created action const welcomeMessageHintText = Localize.translateLocal('accessibilityHints.chatWelcomeMessage'); @@ -241,7 +242,7 @@ describe('Unread Indicators', () => { // Navigate to the unread chat from the sidebar .then(() => navigateToSidebarOption(0)) .then(async () => { - await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); + await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); // Verify the unread indicator is present const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); @@ -361,7 +362,7 @@ describe('Unread Indicators', () => { }) .then(waitForBatchedUpdates) .then(async () => { - await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); + await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText); @@ -379,7 +380,7 @@ describe('Unread Indicators', () => { return signInAndGetAppWithUnreadChat() .then(() => navigateToSidebarOption(0)) - .then(async () => act(() => transitionEndCB?.())) + .then(async () => act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd())) .then(async () => { const reportActionsViewWrapper = await screen.findByTestId('report-actions-view-wrapper'); if (reportActionsViewWrapper) { @@ -484,7 +485,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(async () => { - await act(() => (NativeNavigation as TestHelper.NativeNavigationMock).triggerTransitionEnd()); + await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 7862c1b07be2..c6e023c3b36b 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,4 +1,3 @@ -import type * as NativeNavigation from '@react-navigation/native'; import {Str} from 'expensify-common'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; @@ -33,10 +32,6 @@ type FormData = { entries: () => Array<[string, string | Blob]>; }; -type NativeNavigationMock = typeof NativeNavigation & { - triggerTransitionEnd: () => void; -}; - function setupApp() { beforeAll(() => { Linking.setInitialURL('https://new.expensify.com/'); @@ -310,39 +305,11 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } -type Listener = () => void; - -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * @returns An object with two functions: triggerTransitionEnd and addListener - */ -function createAddListenerMock() { - const transitionEndListeners: Listener[] = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -} - -export type {MockFetch, FormData, NativeNavigationMock}; +export type {MockFetch, FormData}; export { assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, - createAddListenerMock, getGlobalFetchMock, setupApp, setupGlobalFetchMock, diff --git a/tests/utils/createAddListenerMock.ts b/tests/utils/createAddListenerMock.ts new file mode 100644 index 000000000000..6196dd7296e4 --- /dev/null +++ b/tests/utils/createAddListenerMock.ts @@ -0,0 +1,28 @@ +type Listener = () => void; + +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * @returns An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners: Listener[] = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +export default createAddListenerMock; From ef6ba105b93e10102231b039eabc7abb87dfb13d Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 20 Jun 2024 17:31:32 -0400 Subject: [PATCH 157/512] Finalize GetNewerActions test assertions --- tests/ui/PaginationTest.tsx | 42 +++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 8c78ad13ec4c..5c370d2dc54d 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -11,6 +11,7 @@ import * as User from '@userActions/User'; import App from '@src/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction} from '@src/types/onyx'; import type {NativeNavigationMock} from '../../__mocks__/@react-navigation/native'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -51,7 +52,7 @@ function getReportScreen(reportID = REPORT_ID) { function scrollToOffset(offset: number) { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); - fireEvent.scroll(screen.getByLabelText(hintText), { + fireEvent.scroll(within(getReportScreen()).getByLabelText(hintText), { nativeEvent: { contentOffset: { y: offset, @@ -113,14 +114,17 @@ function buildCreatedAction(reportActionID: string, created: string) { function buildReportComments(count: number, initialID: string, reverse = false) { let currentID = parseInt(initialID, 10); - return Object.fromEntries( - Array.from({length: Math.min(count, currentID)}).map(() => { - const created = format(addSeconds(TEN_MINUTES_AGO, 10 * currentID), CONST.DATE.FNS_DB_FORMAT_STRING); - const id = currentID; - currentID += reverse ? 1 : -1; - return [`${id}`, id === 1 ? buildCreatedAction('1', created) : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${id}`)]; - }), - ); + const result: Record> = {}; + for (let i = 0; i < count; i++) { + if (currentID < 1) { + break; + } + const created = format(addSeconds(TEN_MINUTES_AGO, 10 * currentID), CONST.DATE.FNS_DB_FORMAT_STRING); + const id = currentID; + currentID += reverse ? 1 : -1; + result[`${id}`] = id === 1 ? buildCreatedAction('1', created) : TestHelper.buildTestReportComment(created, USER_B_ACCOUNT_ID, `${id}`); + } + return result; } function mockOpenReport(messageCount: number, initialID: string) { @@ -300,12 +304,13 @@ describe('Pagination', () => { await waitForBatchedUpdatesWithAct(); + // We now have 10 messages. 5 from the initial OpenReport and 3 from GetOlderActions. + // GetOlderActions only returns 3 actions since it reaches id '1', which is the created action. expect(getReportActions()).toHaveLength(8); }); it('opens a chat and load newer messages', async () => { mockOpenReport(5, '5'); - mockGetOlderActions(5); mockGetNewerActions(5); await signInAndGetApp(); @@ -319,10 +324,25 @@ describe('Pagination', () => { // ReportScreen relies on the onLayout event to receive updates from onyx. triggerListLayout(); + expect(getReportActions()).toHaveLength(5); + + // There is 1 extra call here because of the comment linking report. TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 2); TestHelper.expectAPICommandToHaveBeenCalledWith('OpenReport', 1, {reportID: REPORT_ID, reportActionID: '5'}); TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 0); - expect(getReportActions()).toHaveLength(5); + + scrollToOffset(0); + await waitForBatchedUpdatesWithAct(); + + TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 2); + TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1); + TestHelper.expectAPICommandToHaveBeenCalledWith('GetNewerActions', 0, {reportID: REPORT_ID, reportActionID: '5'}); + + await waitForBatchedUpdatesWithAct(); + + // We now have 10 messages. 5 from the initial OpenReport and 5 from GetNewerActions. + expect(getReportActions()).toHaveLength(10); }); }); From 0fe243d0facbd977e03b37e045aedfa1d3c03095 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 21 Jun 2024 00:38:56 -0400 Subject: [PATCH 158/512] Add more mergeContinuousPages tests --- src/libs/PaginationUtils.ts | 2 +- tests/unit/PaginationUtilsTest.ts | 78 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/libs/PaginationUtils.ts b/src/libs/PaginationUtils.ts index 31d765399428..ac2621501002 100644 --- a/src/libs/PaginationUtils.ts +++ b/src/libs/PaginationUtils.ts @@ -106,7 +106,7 @@ function mergeContinuousPages(sortedItems: TResource[], pages: Pages, // Pages need to be sorted by firstIndex ascending then by lastIndex descending const sortedPages = pagesWithIndexes.sort((a, b) => { - if (a.firstIndex !== b.firstIndex) { + if (a.firstIndex !== b.firstIndex || a.firstID !== b.firstID) { if (a.firstID === CONST.PAGINATION_START_ID) { return -1; } diff --git a/tests/unit/PaginationUtilsTest.ts b/tests/unit/PaginationUtilsTest.ts index 93f5f888ed06..a47e9aff4ccf 100644 --- a/tests/unit/PaginationUtilsTest.ts +++ b/tests/unit/PaginationUtilsTest.ts @@ -486,5 +486,83 @@ describe('PaginationUtils', () => { const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); expect(result).toStrictEqual(expectedResult); }); + + it('handles page start markers', () => { + const sortedItems = createItems([ + // Given these sortedItems + '1', + '2', + ]); + const pages = [ + // Given these pages + ['1', '2'], + [CONST.PAGINATION_START_ID, '1'], + ]; + const expectedResult = [ + // Expect these pages + [CONST.PAGINATION_START_ID, '1', '2'], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles page end markers', () => { + const sortedItems = createItems([ + // Given these sortedItems + '1', + '2', + ]); + const pages = [ + // Given these pages + ['2', CONST.PAGINATION_END_ID], + ['1', '2'], + ]; + const expectedResult = [ + // Expect these pages + ['1', '2', CONST.PAGINATION_END_ID], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles both page markers', () => { + const sortedItems = createItems([ + // Given these sortedItems + '1', + '2', + '3', + ]); + const pages = [ + // Given these pages + [CONST.PAGINATION_START_ID, '1', '2', '3', CONST.PAGINATION_END_ID], + [CONST.PAGINATION_START_ID, '2', CONST.PAGINATION_END_ID], + ]; + const expectedResult = [ + // Expect these pages + [CONST.PAGINATION_START_ID, '1', '2', '3', CONST.PAGINATION_END_ID], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); + + it('handles mixed page markers', () => { + const sortedItems = createItems([ + // Given these sortedItems + '1', + '2', + '3', + ]); + const pages = [ + // Given these pages + [CONST.PAGINATION_START_ID, '1', '2', '3'], + ['2', '3', CONST.PAGINATION_END_ID], + ]; + const expectedResult = [ + // Expect these pages + [CONST.PAGINATION_START_ID, '1', '2', '3', CONST.PAGINATION_END_ID], + ]; + const result = PaginationUtils.mergeContinuousPages(sortedItems, pages, getID); + expect(result).toStrictEqual(expectedResult); + }); }); }); From c42fcf555926da318d6a1903a07c6a3b63e3f2f5 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 21 Jun 2024 07:23:25 +0200 Subject: [PATCH 159/512] feat: integrate retry billing button --- src/ONYXKEYS.ts | 8 +++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/API/types.ts | 1 + .../AppNavigator/getPartialStateDiff.ts | 4 +- src/libs/actions/Subscription.ts | 53 ++++++++++++++++++- .../Subscription/CardSection/CardSection.tsx | 2 + 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8a20032b4f91..ca98e9ebbe0a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -157,6 +157,12 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store retry billing successful status */ + SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', + + /** Store retry billing failed status */ + SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', @@ -672,6 +678,8 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; diff --git a/src/languages/en.ts b/src/languages/en.ts index 99ed3265f02b..af7516af6dcc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3234,6 +3234,8 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', + success: 'Success!', + yourCardHasBeenBilled: 'Your card has been billed successfully.', }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index 96346458af37..87a5e3b182dc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3738,6 +3738,8 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', + success: 'Éxito!', + yourCardHasBeenBilled: 'Tu tarjeta fue facturada correctamente.', }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8f093ee827c3..280f1c58306a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -227,6 +227,7 @@ const WRITE_COMMANDS = { UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', + CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance', } as const; type WriteCommand = ValueOf; diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts index 5061c7500742..17a8ee158219 100644 --- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts +++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts @@ -72,8 +72,8 @@ function getPartialStateDiff(state: State, templateState: St (!stateTopmostFullScreen && templateStateTopmostFullScreen) || (stateTopmostFullScreen && templateStateTopmostFullScreen && - stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name && - !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined)) + (stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name || + !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined))) ) { diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR] = fullScreenDiff; } diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 19a3bf0c547e..46d71e9f3b81 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -231,4 +231,55 @@ function clearUpdateSubscriptionSizeError() { }); } -export {openSubscriptionPage, updateSubscriptionAutoRenew, updateSubscriptionAddNewUsersAutomatically, updateSubscriptionSize, clearUpdateSubscriptionSizeError, updateSubscriptionType}; +function clearOutstandingBalance() { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + value: false, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + value: false, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: false, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + ], + }; + + API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); +} + +export { + openSubscriptionPage, + updateSubscriptionAutoRenew, + updateSubscriptionAddNewUsersAutomatically, + updateSubscriptionSize, + clearUpdateSubscriptionSizeError, + updateSubscriptionType, + clearOutstandingBalance, +}; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 7f80b189c517..3103b85363d5 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -20,6 +20,8 @@ function CardSection() { const styles = useThemeStyles(); const theme = useTheme(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [retryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {initWithStoredValues: false}); + const [retryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]); From 68fcbcd5fdc4d31e45b6da66c829dfed50cb5b0a Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 21 Jun 2024 13:42:08 +0200 Subject: [PATCH 160/512] before rebase --- src/ONYXKEYS.ts | 6 ----- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/libs/SubscriptionUtils.ts | 1 + src/libs/actions/Subscription.ts | 17 +++++--------- .../CardSection/BillingBanner.tsx | 20 +++++++++++++++++ .../Subscription/CardSection/CardSection.tsx | 22 +++++++++++++++---- .../Subscription/CardSection/utils.ts | 7 +++--- 8 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0eff19d471ef..da6f58cc705c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -169,12 +169,6 @@ const ONYXKEYS = { /** Store the billing status */ NVP_PRIVATE_BILLING_STATUS: 'nvp_private_billingStatus', - /** Store retry billing successful status */ - SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', - - /** Store retry billing failed status */ - SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', - /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', diff --git a/src/languages/en.ts b/src/languages/en.ts index edf83a1725f7..ad1509f1ac85 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3248,8 +3248,6 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', - success: 'Success!', - yourCardHasBeenBilled: 'Your card has been billed successfully.', }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index ca00913d573f..eb828882e192 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3753,8 +3753,6 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', - success: 'Éxito!', - yourCardHasBeenBilled: 'Tu tarjeta fue facturada correctamente.', }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 988c83354efb..c924332dca7b 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -108,6 +108,7 @@ Onyx.connect({ let billingStatusSuccessful: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]; Onyx.connect({ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + initWithStoredValues: false, callback: (value) => { if (value === undefined) { return; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 46d71e9f3b81..558e4c0284a8 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -233,18 +233,6 @@ function clearUpdateSubscriptionSizeError() { function clearOutstandingBalance() { const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, - value: true, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, - value: false, - }, - ], successData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -274,6 +262,10 @@ function clearOutstandingBalance() { API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); } +function resetRetryBillingStatus() { + Onyx.merge(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, false); +} + export { openSubscriptionPage, updateSubscriptionAutoRenew, @@ -282,4 +274,5 @@ export { clearUpdateSubscriptionSizeError, updateSubscriptionType, clearOutstandingBalance, + resetRetryBillingStatus, }; diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx index b3e4d8859249..8ea459b12aad 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx @@ -3,10 +3,14 @@ import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import * as Subscription from '@userActions/Subscription'; +import CONST from '@src/CONST'; type BillingBannerProps = { title?: string; @@ -20,6 +24,7 @@ type BillingBannerProps = { function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isTrialActive}: BillingBannerProps) { const styles = useThemeStyles(); const theme = useTheme(); + const {translate} = useLocalize(); const backgroundStyle = isTrialActive ? styles.trialBannerBackgroundColor : styles.hoveredComponentBG; @@ -48,6 +53,21 @@ function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, sho fill={theme.success} /> )} + {!isError && ( + { + Subscription.resetRetryBillingStatus(); + }} + style={[styles.touchableButtonImage]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + + )} ); } diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 5817e59ad60d..ad17ccddf369 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,13 +1,16 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import * as Subscription from '@userActions/Subscription'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import BillingBanner from './BillingBanner'; import CardSectionActions from './CardSectionActions'; @@ -18,14 +21,13 @@ function CardSection() { const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const [retryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {initWithStoredValues: false}); - const [retryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED); + const {isOffline} = useNetwork(); const defaultCard = CardSectionUtils.getCardForSubscriptionBilling(); const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator} = CardSectionUtils.getBillingStatus( + const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isRetryAvailable} = CardSectionUtils.getBillingStatus( translate, preferredLocale, defaultCard?.accountData?.cardNumber ?? '', @@ -76,7 +78,19 @@ function CardSection() { )} - {isEmptyObject(defaultCard?.accountData) && } + {!isEmptyObject(defaultCard?.accountData) && } + {!isRetryAvailable && ( +