From 935591acdbd0538c1a27baf258c00f33b463e54c Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 20 Dec 2024 01:11:22 +0700 Subject: [PATCH 001/201] feat: add distance label --- .../DistanceRequest/DistanceRequestFooter.tsx | 1 + .../MapView/MapViewImpl.website.tsx | 32 +++++++++++++++++++ src/components/MapView/MapViewTypes.ts | 2 ++ src/libs/DistanceRequestUtils.ts | 6 ++++ 4 files changed, 41 insertions(+) diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx index 8a4455e02bd6..d25037ec808a 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx @@ -114,6 +114,7 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig waypoints={waypointMarkers} styleURL={CONST.MAPBOX.STYLE_URL} overlayStyle={styles.mapEditView} + distance={TransactionUtils.getDistanceInMeters(transaction, 'km')} /> diff --git a/src/components/MapView/MapViewImpl.website.tsx b/src/components/MapView/MapViewImpl.website.tsx index 611c1b117fbf..31fdf0365a58 100644 --- a/src/components/MapView/MapViewImpl.website.tsx +++ b/src/components/MapView/MapViewImpl.website.tsx @@ -16,6 +16,7 @@ import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; import * as UserLocation from '@userActions/UserLocation'; @@ -42,6 +43,7 @@ const MapViewImpl = forwardRef( directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}, interactive = true, + distance, }, ref, ) => { @@ -232,6 +234,20 @@ const MapViewImpl = forwardRef( }; }, [waypoints, directionCoordinates, interactive, currentPosition, initialState.zoom]); + const distanceSymbolCoorinate = useMemo(() => { + const length = directionCoordinates?.length; + // If the array is empty, return undefined + if (!length) { + return undefined; + } + + // Find the index of the middle element + const middleIndex = Math.floor(length / 2); + + // Return the middle element + return directionCoordinates.at(middleIndex); + }, [directionCoordinates]); + return !isOffline && !!accessToken && !!initialViewState ? ( ( )} + {!!distanceSymbolCoorinate && !!distance && ( + + + + {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS)} + {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES)} + + + + )} {waypoints?.map(({coordinate, markerComponent, id}) => { const MarkerComponent = markerComponent; if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) { diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts index 41170694c9d2..14104f79779b 100644 --- a/src/components/MapView/MapViewTypes.ts +++ b/src/components/MapView/MapViewTypes.ts @@ -22,6 +22,8 @@ type MapViewProps = { onMapReady?: () => void; // Whether the map is interactable or not interactive?: boolean; + + distance?: number; }; type DirectionProps = { diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index fe40ea67f905..31d838af69d6 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -202,6 +202,11 @@ function getDistanceForDisplay( return `${distanceInUnits} ${unitString}`; } +function getDistanceForDisplayLabel(distanceInMeters: number, unit: Unit): string { + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); + return `${distanceInUnits} ${unit}`; +} + /** * @param hasRoute Whether the route exists for the distance expense * @param distanceInMeters Distance traveled @@ -394,6 +399,7 @@ export default { getUpdatedDistanceUnit, getRate, getRateByCustomUnitRateID, + getDistanceForDisplayLabel, }; export type {MileageRate}; From e21733748c4604638a5919105692b05f0c6e2936 Mon Sep 17 00:00:00 2001 From: John Schuster Date: Fri, 20 Dec 2024 13:43:29 -0600 Subject: [PATCH 002/201] Update Configure-Netsuite.md --- .../connections/netsuite/Configure-Netsuite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index 68bca5228913..3e464f82d723 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -59,7 +59,7 @@ This dictates when reimbursable expenses will export, according to your preferre **Journal Entries:** Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your workspace's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite. -- Expensify Card expenses will always export as Journal Entries, even if you have Expense Reports or Vendor Bills configured for non-reimbursable expenses on the Export tab +- Expensify Card expenses always export as individual, itemized Journal Entries, regardless of Expense Reports or Vendor Bills settings configured for non-reimbursable expenses on the Export tab. - Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option - The credit line and header level classifications are pulled from the employee record From f250dcf8b4a792b2bba9447a763083b56db02343 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 7 Jan 2025 16:00:52 +0700 Subject: [PATCH 003/201] fix: border around workspace not matching hovered background --- src/pages/home/report/ReportActionItemSingle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 3d4c1fdadbda..2288f1db42ea 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -215,6 +215,7 @@ function ReportActionItemSingle({ mainAvatar={icon} secondaryAvatar={secondaryAvatar} noMargin + backgroundColor={isHovered ? styles.sidebarLinkActive.backgroundColor : theme.sidebar} /> ); } From 8542fd5e4872d283258cce2053ad0f0515e7db3c Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 13 Jan 2025 15:34:16 +0700 Subject: [PATCH 004/201] Add active case --- src/pages/home/report/PureReportActionItem.tsx | 1 + src/pages/home/report/ReportActionItemSingle.tsx | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index dd44558f83bf..cba395570a95 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -996,6 +996,7 @@ function PureReportActionItem({ report={report} iouReport={iouReport} isHovered={hovered} + isActive={!!isReportActionLinked} hasBeenFlagged={ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && !ReportActionsUtils.isPendingRemove(action) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 2288f1db42ea..25f8b33379e0 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -57,6 +57,9 @@ type ReportActionItemSingleProps = Partial & { /** If the action is being hovered */ isHovered?: boolean; + + /** If the action is being actived */ + isActive?: boolean; }; const showUserDetails = (accountID: string) => { @@ -80,6 +83,7 @@ function ReportActionItemSingle({ report, iouReport, isHovered = false, + isActive = false, }: ReportActionItemSingleProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -208,6 +212,15 @@ function ReportActionItemSingle({ [action, isWorkspaceActor, actorAccountID], ); + const getBackgroundColor = () => { + if (isActive) { + return theme.messageHighlightBG; + } + if (isHovered) { + return styles.sidebarLinkActive.backgroundColor; + } + return theme.sidebar; + }; const getAvatar = () => { if (shouldShowSubscriptAvatar) { return ( @@ -215,7 +228,7 @@ function ReportActionItemSingle({ mainAvatar={icon} secondaryAvatar={secondaryAvatar} noMargin - backgroundColor={isHovered ? styles.sidebarLinkActive.backgroundColor : theme.sidebar} + backgroundColor={getBackgroundColor()} /> ); } From cacb979d9cbf4dead5a89812a94df4ed16a8e06d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 15 Jan 2025 01:49:44 +0700 Subject: [PATCH 005/201] fix reply border when active --- src/components/MultipleAvatars.tsx | 6 +++++- src/pages/home/report/PureReportActionItem.tsx | 1 + src/pages/home/report/ReportActionItemSingle.tsx | 6 +++--- src/pages/home/report/ReportActionItemThread.tsx | 6 +++++- src/styles/utils/index.ts | 13 ++++++++++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index f8169503f932..6ce785fbdc73 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -37,6 +37,9 @@ type MultipleAvatarsProps = { /** Whether the avatars are hovered */ isHovered?: boolean; + /** Whether the avatars are actived */ + isActive?: boolean; + /** Whether the avatars are in an element being pressed */ isPressed?: boolean; @@ -76,6 +79,7 @@ function MultipleAvatars({ shouldStackHorizontally = false, shouldDisplayAvatarsInRows = false, isHovered = false, + isActive = false, isPressed = false, isFocusMode = false, isInReportAction = false, @@ -173,7 +177,6 @@ function MultipleAvatars({ const oneAvatarSize = StyleUtils.getAvatarStyle(size); const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(size).borderWidth ?? 0; const overlapSize = oneAvatarSize.width / overlapDivider; - if (shouldStackHorizontally) { // Height of one avatar + border space const height = oneAvatarSize.height + 2 * oneAvatarBorderWidth; @@ -206,6 +209,7 @@ function MultipleAvatars({ isPressed, isInReportAction, shouldUseCardBackground, + isActive, }), StyleUtils.getAvatarBorderWidth(size), ]} diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index cba395570a95..ff471583a483 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -964,6 +964,7 @@ function PureReportActionItem({ isHovered={hovered} icons={ReportUtils.getIconsForParticipants(oldestFourAccountIDs, personalDetails)} onSecondaryInteraction={showPopover} + isActive={!!isReportActionLinked} /> )} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 25f8b33379e0..853e830323fc 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -213,12 +213,12 @@ function ReportActionItemSingle({ ); const getBackgroundColor = () => { - if (isActive) { - return theme.messageHighlightBG; - } if (isHovered) { return styles.sidebarLinkActive.backgroundColor; } + if (isActive) { + return theme.messageHighlightBG; + } return theme.sidebar; }; const getAvatar = () => { diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index 13072a653749..b3a39da997dc 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -28,11 +28,14 @@ type ReportActionItemThreadProps = { /** Whether the thread item / message is being hovered */ isHovered: boolean; + /** Whether the thread item / message is being actived */ + isActive?: boolean; + /** The function that should be called when the thread is LongPressed or right-clicked */ onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void; }; -function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) { +function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction, isActive}: ReportActionItemThreadProps) { const styles = useThemeStyles(); const {translate, datetimeToCalendarTime} = useLocalize(); @@ -60,6 +63,7 @@ function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childR icons={icons} shouldStackHorizontally isHovered={isHovered} + isActive={isActive} isInReportAction /> diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 3bb80f71f1b5..1f3cd3ee63f5 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -762,14 +762,25 @@ type AvatarBorderStyleParams = { isPressed: boolean; isInReportAction: boolean; shouldUseCardBackground: boolean; + isActive?: boolean; }; -function getHorizontalStackedAvatarBorderStyle({theme, isHovered, isPressed, isInReportAction = false, shouldUseCardBackground = false}: AvatarBorderStyleParams): ViewStyle { +function getHorizontalStackedAvatarBorderStyle({ + theme, + isHovered, + isPressed, + isInReportAction = false, + shouldUseCardBackground = false, + isActive = false, +}: AvatarBorderStyleParams): ViewStyle { let borderColor = shouldUseCardBackground ? theme.cardBG : theme.appBG; if (isHovered) { borderColor = isInReportAction ? theme.hoverComponentBG : theme.border; } + if (isActive) { + borderColor = theme.messageHighlightBG; + } if (isPressed) { borderColor = isInReportAction ? theme.hoverComponentBG : theme.buttonPressedBG; From 570156fbaff290b552e2d78ff7a16f2c0f795aaa Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 15 Jan 2025 17:07:14 +0700 Subject: [PATCH 006/201] refactor TrackExpense function --- src/libs/actions/IOU.ts | 81 ++++++++++++------- .../iou/request/step/IOURequestStepAmount.tsx | 26 +++--- .../step/IOURequestStepConfirmation.tsx | 56 +++++++------ .../step/IOURequestStepScan/index.native.tsx | 74 +++++++++-------- .../request/step/IOURequestStepScan/index.tsx | 74 +++++++++-------- tests/unit/GoogleTagManagerTest.tsx | 51 ++++++------ 6 files changed, 200 insertions(+), 162 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0ac396709b07..a590acec5d48 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -402,6 +402,35 @@ type CreateDistanceRequestInformation = { policyParams?: RequestMoneyPolicyParams; }; +type TrackExpenseTransactionParams = { + amount: number; + currency: string; + created: string | undefined; + merchant: string; + comment: string; + receipt?: Receipt; + category?: string; + tag?: string; + taxCode?: string; + taxAmount?: number; + billable?: boolean; + validWaypoints?: WaypointCollection; + gpsPoints?: GPSPoint; + actionableWhisperReportActionID?: string; + linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction; + linkedTrackedExpenseReportID?: string; + customUnitRateID?: string; +}; + +type CreateTrackExpenseParams = { + report: OnyxTypes.Report; + isDraftPolicy: boolean; + action?: IOUAction; + participantParams: RequestMoneyParticipantParams; + policyParams?: RequestMoneyPolicyParams; + transactionParams: TrackExpenseTransactionParams; +}; + let allPersonalDetails: OnyxTypes.PersonalDetailsList = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -4154,34 +4183,30 @@ function sendInvoice( /** * Track an expense */ -function trackExpense( - report: OnyxTypes.Report, - amount: number, - currency: string, - created: string, - merchant: string, - payeeEmail: string | undefined, - payeeAccountID: number, - participant: Participant, - comment: string, - isDraftPolicy: boolean, - receipt?: Receipt, - category?: string, - tag?: string, - taxCode = '', - taxAmount = 0, - billable?: boolean, - policy?: OnyxEntry, - policyTagList?: OnyxEntry, - policyCategories?: OnyxEntry, - gpsPoints?: GPSPoint, - validWaypoints?: WaypointCollection, - action?: IOUAction, - actionableWhisperReportActionID?: string, - linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction, - linkedTrackedExpenseReportID?: string, - customUnitRateID?: string, -) { +function trackExpense(params: CreateTrackExpenseParams) { + const {report, action, isDraftPolicy, participantParams, policyParams: policyData = {}, transactionParams: transactionData} = params; + const {participant, payeeAccountID, payeeEmail} = participantParams; + const {policy, policyCategories, policyTagList} = policyData; + const { + amount, + currency, + created = '', + merchant, + comment, + receipt, + category, + tag, + taxCode = '', + taxAmount = 0, + billable, + gpsPoints, + validWaypoints, + actionableWhisperReportActionID, + linkedTrackedExpenseReportAction, + linkedTrackedExpenseReportID, + customUnitRateID, + } = transactionData; + const isMoneyRequestReport = isMoneyRequestReportReportUtils(report); const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 4814cd184ddf..776cded37d1b 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -208,18 +208,22 @@ function IOURequestStepAmount({ } if (iouType === CONST.IOU.TYPE.TRACK) { playSound(SOUNDS.DONE); - IOU.trackExpense( + IOU.trackExpense({ report, - backendAmount, - currency ?? 'USD', - transaction?.created ?? '', - CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participants.at(0) ?? {}, - '', - false, - ); + isDraftPolicy: false, + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant: participants.at(0) ?? {}, + }, + transactionParams: { + amount: backendAmount, + currency: currency ?? 'USD', + created: transaction?.created, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + comment: '', + }, + }); return; } } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 3c24f317c812..189932394ebc 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -275,34 +275,40 @@ function IOURequestStepConfirmation({ if (!participant) { return; } - IOU.trackExpense( + IOU.trackExpense({ report, - transaction.amount, - transaction.currency, - transaction.created, - transaction.merchant, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - trimmedComment, isDraftPolicy, - receiptObj, - transaction.category, - transaction.tag, - transactionTaxCode, - transactionTaxAmount, - transaction.billable, - policy, - policyTags, - policyCategories, - gpsPoints, - Object.keys(transaction?.comment?.waypoints ?? {}).length ? TransactionUtils.getValidWaypoints(transaction.comment?.waypoints, true) : undefined, action, - transaction.actionableWhisperReportActionID, - transaction.linkedTrackedExpenseReportAction, - transaction.linkedTrackedExpenseReportID, - customUnitRateID, - ); + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + }, + policyParams: { + policy, + policyCategories, + policyTagList: policyTags, + }, + transactionParams: { + amount: transaction.amount, + currency: transaction.currency, + created: transaction.created, + merchant: transaction.merchant, + comment: trimmedComment, + receipt: receiptObj, + category: transaction.category, + tag: transaction.tag, + taxCode: transactionTaxCode, + taxAmount: transactionTaxAmount, + billable: transaction.billable, + gpsPoints, + validWaypoints: Object.keys(transaction?.comment?.waypoints ?? {}).length ? TransactionUtils.getValidWaypoints(transaction.comment?.waypoints, true) : undefined, + actionableWhisperReportActionID: transaction.actionableWhisperReportActionID, + linkedTrackedExpenseReportAction: transaction.linkedTrackedExpenseReportAction, + linkedTrackedExpenseReportID: transaction.linkedTrackedExpenseReportID, + customUnitRateID, + }, + }); }, [ report, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 180868c8ca51..cae8b46af6bb 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -246,19 +246,23 @@ function IOURequestStepScan({ const createTransaction = useCallback( (receipt: Receipt, participant: Participant) => { if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense( + IOU.trackExpense({ report, - 0, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - false, - receipt, - ); + isDraftPolicy: false, + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + }, + transactionParams: { + amount: 0, + currency: transaction?.currency ?? 'USD', + created: transaction?.created, + merchant: '', + comment: '', + receipt, + }, + }); } else { IOU.requestMoney({ report, @@ -334,31 +338,31 @@ function IOURequestStepScan({ (successData) => { playSound(SOUNDS.DONE); if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense( + IOU.trackExpense({ report, - 0, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - false, - receipt, - '', - '', - '', - 0, - false, - policy, - {}, - {}, - { - lat: successData.coords.latitude, - long: successData.coords.longitude, + isDraftPolicy: false, + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, }, - ); + policyParams: { + policy, + }, + transactionParams: { + amount: 0, + currency: transaction?.currency ?? 'USD', + created: transaction?.created, + merchant: '', + comment: '', + receipt, + billable: false, + gpsPoints: { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }, + }, + }); } else { IOU.requestMoney({ report, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 8dbe72d3f9fa..692381d3301f 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -271,19 +271,23 @@ function IOURequestStepScan({ const createTransaction = useCallback( (receipt: Receipt, participant: Participant) => { if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense( + IOU.trackExpense({ report, - 0, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - false, - receipt, - ); + isDraftPolicy: false, + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + }, + transactionParams: { + amount: 0, + currency: transaction?.currency ?? 'USD', + created: transaction?.created, + merchant: '', + comment: '', + receipt, + }, + }); } else { IOU.requestMoney({ report, @@ -360,31 +364,31 @@ function IOURequestStepScan({ (successData) => { playSound(SOUNDS.DONE); if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense( + IOU.trackExpense({ report, - 0, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - false, - receipt, - '', - '', - '', - 0, - false, - policy, - {}, - {}, - { - lat: successData.coords.latitude, - long: successData.coords.longitude, + isDraftPolicy: false, + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, }, - ); + policyParams: { + policy, + }, + transactionParams: { + amount: 0, + currency: transaction?.currency ?? 'USD', + created: transaction?.created, + merchant: '', + comment: '', + receipt, + billable: false, + gpsPoints: { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }, + }, + }); } else { IOU.requestMoney({ report, diff --git a/tests/unit/GoogleTagManagerTest.tsx b/tests/unit/GoogleTagManagerTest.tsx index dcb6bdea0eec..9a2041634443 100644 --- a/tests/unit/GoogleTagManagerTest.tsx +++ b/tests/unit/GoogleTagManagerTest.tsx @@ -67,34 +67,29 @@ describe('GoogleTagManagerTest', () => { }); test('workspace_created - categorizeTrackedExpense', () => { - // When we categorize a tracked expense with a draft policy - IOU.trackExpense( - {reportID: '123'}, - 1000, - 'USD', - '2024-10-30', - 'merchant', - undefined, - 0, - {accountID}, - 'comment', - true, - undefined, - 'category', - 'tag', - 'taxCode', - 0, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - CONST.IOU.ACTION.CATEGORIZE, - 'actionableWhisperReportActionID', - {actionName: 'IOU', reportActionID: 'linkedTrackedExpenseReportAction', created: '2024-10-30'}, - 'linkedTrackedExpenseReportID', - ); + IOU.trackExpense({ + report: {reportID: '123'}, + isDraftPolicy: true, + action: CONST.IOU.ACTION.CATEGORIZE, + participantParams: { + payeeEmail: undefined, + payeeAccountID: 0, + participant: {accountID}, + }, + transactionParams: { + amount: 1000, + currency: 'USD', + created: '2024-10-30', + merchant: 'merchant', + comment: 'comment', + category: 'category', + tag: 'tag', + taxCode: 'taxCode', + actionableWhisperReportActionID: 'actionableWhisperReportActionID', + linkedTrackedExpenseReportAction: {actionName: 'IOU', reportActionID: 'linkedTrackedExpenseReportAction', created: '2024-10-30'}, + linkedTrackedExpenseReportID: 'linkedTrackedExpenseReportID', + }, + }); // Then we publish a workspace_created event only once expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); From 988005f8e29ffdd12d90563f87d155ea018710cb Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 15 Jan 2025 17:30:42 +0700 Subject: [PATCH 007/201] fix: style web --- src/components/MapView/MapViewImpl.website.tsx | 8 +++----- src/styles/index.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/MapView/MapViewImpl.website.tsx b/src/components/MapView/MapViewImpl.website.tsx index 31fdf0365a58..cdeee9b4b7da 100644 --- a/src/components/MapView/MapViewImpl.website.tsx +++ b/src/components/MapView/MapViewImpl.website.tsx @@ -280,11 +280,9 @@ const MapViewImpl = forwardRef( latitude={distanceSymbolCoorinate.at(1) ?? 0} > - - {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS)} - {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES)} + + {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS)} + {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES)} diff --git a/src/styles/index.ts b/src/styles/index.ts index d01aef6469f4..56146a0fa883 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4005,6 +4005,16 @@ const styles = (theme: ThemeColors) => paddingVertical: 8, borderRadius: variables.componentBorderRadiusMedium, }, + distanceLabelWrapper: { + backgroundColor: colors.green400, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + textAlign: 'center', + fontSize: 13, + fontWeight: FontUtils.fontWeight.bold, + color: colors.productLight100, + }, productTrainingTooltipWrapper: { backgroundColor: theme.tooltipHighlightBG, From 738ab3e396a2e70ba691b22d4ba6c2bea1732307 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 15 Jan 2025 21:36:39 +0700 Subject: [PATCH 008/201] update all trackExpense function --- .../request/step/IOURequestStepDistance.tsx | 49 +++--- tests/actions/IOUTest.ts | 156 ++++++++---------- 2 files changed, 94 insertions(+), 111 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 9a136f514a9b..e2855ba32eea 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -322,34 +322,29 @@ function IOURequestStepDistance({ const participant = participants.at(0); if (iouType === CONST.IOU.TYPE.TRACK && participant) { playSound(SOUNDS.DONE); - trackExpense( + trackExpense({ report, - 0, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - translate('iou.fieldPending'), - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - false, - {}, - '', - '', - '', - 0, - false, - policy, - undefined, - undefined, - undefined, - getValidWaypoints(waypoints, true), - undefined, - undefined, - undefined, - undefined, - customUnitRateID, - ); + isDraftPolicy: false, + participantParams: { + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + }, + policyParams: { + policy, + }, + transactionParams: { + amount: 0, + currency: transaction?.currency ?? 'USD', + created: transaction?.created ?? '', + merchant: translate('iou.fieldPending'), + comment: '', + receipt: {}, + billable: false, + validWaypoints: getValidWaypoints(waypoints, true), + customUnitRateID, + }, + }); return; } diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 291d2d4ac1e3..1d7cb03dcb87 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -190,34 +190,29 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); // When the user submits the transaction to the selfDM report - trackExpense( - selfDMReport, - fakeTransaction.amount, - fakeTransaction.currency, - format(new Date(), CONST.DATE.FNS_FORMAT_STRING), - fakeTransaction.merchant, - participant.login, - participant.accountID, - participant, - '', - true, - undefined, - '', - undefined, - '', - 0, - false, - undefined, - undefined, - undefined, - undefined, - fakeWayPoints, - CONST.IOU.ACTION.CREATE, - fakeTransaction?.actionableWhisperReportActionID, - fakeTransaction?.linkedTrackedExpenseReportAction, - fakeTransaction?.linkedTrackedExpenseReportID, - CONST.CUSTOM_UNITS.FAKE_P2P_ID, - ); + trackExpense({ + report: selfDMReport, + isDraftPolicy: true, + action: CONST.IOU.ACTION.CREATE, + participantParams: { + payeeEmail: participant.login, + payeeAccountID: participant.accountID, + participant, + }, + transactionParams: { + amount: fakeTransaction.amount, + currency: fakeTransaction.currency, + created: format(new Date(), CONST.DATE.FNS_FORMAT_STRING), + merchant: fakeTransaction.merchant, + comment: '', + billable: false, + validWaypoints: fakeWayPoints, + actionableWhisperReportActionID: fakeTransaction?.actionableWhisperReportActionID, + linkedTrackedExpenseReportAction: fakeTransaction?.linkedTrackedExpenseReportAction, + linkedTrackedExpenseReportID: fakeTransaction?.linkedTrackedExpenseReportID, + customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, + }, + }); await waitForBatchedUpdates(); await mockFetch?.resume?.(); @@ -283,34 +278,33 @@ describe('actions/IOU', () => { const transactionDraft = allTransactionsDraft?.[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction?.transactionID}`]; // When the user confirms the category for the tracked expense - trackExpense( - expenseReport, - transactionDraft?.amount ?? fakeTransaction.amount, - transactionDraft?.currency ?? fakeTransaction.currency, - format(new Date(), CONST.DATE.FNS_FORMAT_STRING), - transactionDraft?.merchant ?? fakeTransaction.merchant, - participant.login, - participant.accountID, - {...participant, isPolicyExpenseChat: true}, - '', - false, - undefined, - Object.keys(fakeCategories).at(0) ?? '', - '', - '', - 0, - undefined, - fakePolicy, - undefined, - fakeCategories, - undefined, - Object.keys(transactionDraft?.comment?.waypoints ?? {}).length ? getValidWaypoints(transactionDraft?.comment?.waypoints, true) : undefined, - CONST.IOU.ACTION.CATEGORIZE, - transactionDraft?.actionableWhisperReportActionID, - transactionDraft?.linkedTrackedExpenseReportAction, - transactionDraft?.linkedTrackedExpenseReportID, - CONST.CUSTOM_UNITS.FAKE_P2P_ID, - ); + trackExpense({ + report: expenseReport, + isDraftPolicy: false, + action: CONST.IOU.ACTION.CATEGORIZE, + participantParams: { + payeeEmail: participant.login, + payeeAccountID: participant.accountID, + participant: {...participant, isPolicyExpenseChat: true}, + }, + policyParams: { + policy: fakePolicy, + policyCategories: fakeCategories, + }, + transactionParams: { + amount: transactionDraft?.amount ?? fakeTransaction.amount, + currency: transactionDraft?.currency ?? fakeTransaction.currency, + created: format(new Date(), CONST.DATE.FNS_FORMAT_STRING), + merchant: transactionDraft?.merchant ?? fakeTransaction.merchant, + comment: '', + category: Object.keys(fakeCategories).at(0) ?? '', + validWaypoints: Object.keys(transactionDraft?.comment?.waypoints ?? {}).length ? getValidWaypoints(transactionDraft?.comment?.waypoints, true) : undefined, + actionableWhisperReportActionID: transactionDraft?.actionableWhisperReportActionID, + linkedTrackedExpenseReportAction: transactionDraft?.linkedTrackedExpenseReportAction, + linkedTrackedExpenseReportID: transactionDraft?.linkedTrackedExpenseReportID, + customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, + }, + }); await waitForBatchedUpdates(); await mockFetch?.resume?.(); @@ -4398,37 +4392,31 @@ describe('actions/IOU', () => { [WRITE_COMMANDS.SHARE_TRACKED_EXPENSE, CONST.IOU.ACTION.SHARE], ])('%s', async (expectedCommand: ApiCommand, action: IOUAction) => { // When a track expense is created - trackExpense( - {reportID: ''}, - 10000, - CONST.CURRENCY.USD, - '2024-10-30', - 'KFC', - RORY_EMAIL, - RORY_ACCOUNT_ID, - {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, - '', - false, - {}, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, + trackExpense({ + report: {reportID: ''}, + isDraftPolicy: false, action, - '1', - { - reportActionID: '', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + participantParams: { + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + transactionParams: { + amount: 10000, + currency: CONST.CURRENCY.USD, created: '2024-10-30', + merchant: 'KFC', + comment: '', + receipt: {}, + actionableWhisperReportActionID: '1', + linkedTrackedExpenseReportAction: { + reportActionID: '', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2024-10-30', + }, + linkedTrackedExpenseReportID: '1', }, - '1', - ); + }); await waitForBatchedUpdates(); From c3bd9f11b88dfd305ff8c3f4d6bdc44de755dfd7 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 16 Jan 2025 12:27:47 +0700 Subject: [PATCH 009/201] fix lint --- .../iou/request/step/IOURequestStepAmount.tsx | 89 +++++++++------- .../step/IOURequestStepConfirmation.tsx | 100 ++++++++++-------- .../step/IOURequestStepScan/index.native.tsx | 61 ++++++----- .../request/step/IOURequestStepScan/index.tsx | 73 +++++++------ tests/unit/GoogleTagManagerTest.tsx | 18 ++-- 5 files changed, 189 insertions(+), 152 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 776cded37d1b..3aa83a857530 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -6,22 +6,33 @@ import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import * as TransactionEdit from '@libs/actions/TransactionEdit'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import { + requestMoney, + resetSplitShares, + sendMoneyElsewhere, + sendMoneyWithWallet, + setDraftSplitTransaction, + setMoneyRequestAmount, + setMoneyRequestParticipantsFromReport, + setMoneyRequestTaxAmount, + setSplitShares, + trackExpense, + updateMoneyRequestAmountAndCurrency, +} from '@libs/actions/IOU'; +import {createDraftTransaction, removeDraftTransaction} from '@libs/actions/TransactionEdit'; +import {convertToBackendAmount, isValidCurrencyCode} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {getBankAccountRoute, getTransactionDetails, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import {getRequestType} from '@libs/TransactionUtils'; +import {calculateTaxAmount, getAmount, getCurrency, getDefaultTaxCode, getRequestType, getTaxValue} from '@libs/TransactionUtils'; import MoneyRequestAmountForm from '@pages/iou/MoneyRequestAmountForm'; -import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type Transaction from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -36,7 +47,7 @@ type AmountParams = { type IOURequestStepAmountProps = WithCurrentUserPersonalDetailsProps & WithWritableReportOrNotFoundProps & { /** The transaction object being modified in Onyx */ - transaction: OnyxEntry; + transaction: OnyxEntry; /** Whether the user input should be kept or not */ shouldKeepUserInput?: boolean; @@ -69,9 +80,9 @@ function IOURequestStepAmount({ const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const isEditingSplitBill = isEditing && isSplitBill; const currentTransaction = isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction; - const {amount: transactionAmount} = ReportUtils.getTransactionDetails(currentTransaction) ?? {amount: 0}; - const {currency: originalCurrency} = ReportUtils.getTransactionDetails(isEditing && !isEmptyObject(draftTransaction) ? draftTransaction : transaction) ?? {currency: CONST.CURRENCY.USD}; - const currency = CurrencyUtils.isValidCurrencyCode(selectedCurrency) ? selectedCurrency : originalCurrency; + const {amount: transactionAmount} = getTransactionDetails(currentTransaction) ?? {amount: 0}; + const {currency: originalCurrency} = getTransactionDetails(isEditing && !isEmptyObject(draftTransaction) ? draftTransaction : transaction) ?? {currency: CONST.CURRENCY.USD}; + const currency = isValidCurrencyCode(selectedCurrency) ? selectedCurrency : originalCurrency; // For quick button actions, we'll skip the confirmation page unless the report is archived or this is a workspace request, as // the user will have to add a merchant. @@ -80,7 +91,7 @@ function IOURequestStepAmount({ return false; } - return !(ReportUtils.isArchivedReport(report, reportNameValuePairs) || ReportUtils.isPolicyExpenseChat(report)); + return !(isArchivedReport(report, reportNameValuePairs) || isPolicyExpenseChat(report)); }, [report, isSplitBill, skipConfirmation, reportNameValuePairs]); useFocusEffect( @@ -102,13 +113,13 @@ function IOURequestStepAmount({ // A temporary solution to not prevent users from editing the currency // We create a backup transaction and use it to save the currency and remove this transaction backup if we don't save the amount // It should be removed after this issue https://github.com/Expensify/App/issues/34607 is fixed - TransactionEdit.createDraftTransaction(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction); + createDraftTransaction(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction); return () => { if (isSaveButtonPressed.current) { return; } - TransactionEdit.removeDraftTransaction(transaction?.transactionID); + removeDraftTransaction(transaction?.transactionID); }; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -149,14 +160,14 @@ function IOURequestStepAmount({ const navigateToNextPage = ({amount, paymentMethod}: AmountParams) => { isSaveButtonPressed.current = true; - const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); + const amountInSmallestCurrencyUnits = convertToBackendAmount(Number.parseFloat(amount)); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - IOU.setMoneyRequestAmount(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, shouldKeepUserInput); + setMoneyRequestAmount(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, shouldKeepUserInput); // Initially when we're creating money request, we do not know the participant and hence if the request is with workspace with tax tracking enabled // So, we reset the taxAmount here and calculate it in the hook in MoneyRequestConfirmationList component - IOU.setMoneyRequestTaxAmount(transactionID, null); + setMoneyRequestTaxAmount(transactionID, null); if (backTo) { Navigation.goBack(backTo); @@ -169,27 +180,27 @@ function IOURequestStepAmount({ // In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. // If the user is started this flow using the Create expense option (combined submit/track flow), they should be redirected to the participants page. - if (report?.reportID && !ReportUtils.isArchivedReport(report, reportNameValuePairs) && iouType !== CONST.IOU.TYPE.CREATE) { - const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + if (report?.reportID && !isArchivedReport(report, reportNameValuePairs) && iouType !== CONST.IOU.TYPE.CREATE) { + const selectedParticipants = setMoneyRequestParticipantsFromReport(transactionID, report); const participants = selectedParticipants.map((participant) => { const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID; - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participantAccountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant); }); - const backendAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); + const backendAmount = convertToBackendAmount(Number.parseFloat(amount)); if (shouldSkipConfirmation) { if (iouType === CONST.IOU.TYPE.PAY || iouType === CONST.IOU.TYPE.SEND) { if (paymentMethod && paymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - IOU.sendMoneyWithWallet(report, backendAmount, currency, '', currentUserPersonalDetails.accountID, participants.at(0) ?? {}); + sendMoneyWithWallet(report, backendAmount, currency, '', currentUserPersonalDetails.accountID, participants.at(0) ?? {}); return; } - IOU.sendMoneyElsewhere(report, backendAmount, currency, '', currentUserPersonalDetails.accountID, participants.at(0) ?? {}); + sendMoneyElsewhere(report, backendAmount, currency, '', currentUserPersonalDetails.accountID, participants.at(0) ?? {}); return; } if (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.REQUEST) { playSound(SOUNDS.DONE); - IOU.requestMoney({ + requestMoney({ report, participantParams: { participant: participants.at(0) ?? {}, @@ -208,7 +219,7 @@ function IOURequestStepAmount({ } if (iouType === CONST.IOU.TYPE.TRACK) { playSound(SOUNDS.DONE); - IOU.trackExpense({ + trackExpense({ report, isDraftPolicy: false, participantParams: { @@ -227,10 +238,10 @@ function IOURequestStepAmount({ return; } } - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + setMoneyRequestParticipantsFromReport(transactionID, report); if (isSplitBill && !report.isOwnPolicyExpenseChat && report.participants) { const participantAccountIDs = Object.keys(report.participants).map((accountID) => Number(accountID)); - IOU.setSplitShares(transaction, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, participantAccountIDs); + setSplitShares(transaction, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, participantAccountIDs); } navigateToConfirmationPage(); return; @@ -242,11 +253,11 @@ function IOURequestStepAmount({ }; const saveAmountAndCurrency = ({amount, paymentMethod}: AmountParams) => { - const newAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); + const newAmount = convertToBackendAmount(Number.parseFloat(amount)); // Edits to the amount from the splits page should reset the split shares. if (transaction?.splitShares) { - IOU.resetSplitShares(transaction, newAmount, currency); + resetSplitShares(transaction, newAmount, currency); } if (!isEditing) { @@ -255,26 +266,26 @@ function IOURequestStepAmount({ } // If the value hasn't changed, don't request to save changes on the server and just close the modal - const transactionCurrency = TransactionUtils.getCurrency(currentTransaction); - if (newAmount === TransactionUtils.getAmount(currentTransaction) && currency === transactionCurrency) { + const transactionCurrency = getCurrency(currentTransaction); + if (newAmount === getAmount(currentTransaction) && currency === transactionCurrency) { navigateBack(); return; } // If currency has changed, then we get the default tax rate based on currency, otherwise we use the current tax rate selected in transaction, if we have it. - const transactionTaxCode = ReportUtils.getTransactionDetails(currentTransaction)?.taxCode; - const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, currentTransaction, currency) ?? ''; + const transactionTaxCode = getTransactionDetails(currentTransaction)?.taxCode; + const defaultTaxCode = getDefaultTaxCode(policy, currentTransaction, currency) ?? ''; const taxCode = (currency !== transactionCurrency ? defaultTaxCode : transactionTaxCode) ?? defaultTaxCode; - const taxPercentage = TransactionUtils.getTaxValue(policy, currentTransaction, taxCode) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount, currency ?? CONST.CURRENCY.USD)); + const taxPercentage = getTaxValue(policy, currentTransaction, taxCode) ?? ''; + const taxAmount = convertToBackendAmount(calculateTaxAmount(taxPercentage, newAmount, currency ?? CONST.CURRENCY.USD)); if (isSplitBill) { - IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount}); + setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount}); navigateBack(); return; } - IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount, policy, taxCode}); + updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount, policy, taxCode}); navigateBack(); }; @@ -293,7 +304,7 @@ function IOURequestStepAmount({ skipConfirmation={shouldSkipConfirmation ?? false} iouType={iouType} policyID={policy?.id} - bankAccountRoute={ReportUtils.getBankAccountRoute(report)} + bankAccountRoute={getBankAccountRoute(report)} ref={(e) => (textInput.current = e)} shouldKeepUserInput={transaction?.shouldShowOriginalAmount} onCurrencyButtonPress={navigateToCurrencySelectionPage} diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 189932394ebc..c95647747938 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -15,18 +15,34 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import { + createDistanceRequest as createDistanceRequestFunc, + getIOURequestPolicyID, + navigateToStartStepIfScanFileCannotBeRead, + requestMoney as requestMoneyFunc, + sendInvoice, + sendMoneyElsewhere, + sendMoneyWithWallet, + setMoneyRequestBillable, + setMoneyRequestCategory, + splitBill, + splitBillAndOpenReport, + startSplitBill, + trackExpense as trackExpenseFunc, + updateLastLocationPermissionPrompt, +} from '@libs/actions/IOU'; +import type {GpsPoint} from '@libs/actions/IOU'; import DateUtils from '@libs/DateUtils'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {isLocalFile as isLocalFileUtil} from '@libs/fileDownload/FileUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; -import * as IOUUtils from '@libs/IOUUtils'; +import {isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil, navigateToStartMoneyRequestStep, shouldUseTransactionDraft} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {getBankAccountRoute} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as IOU from '@userActions/IOU'; +import {getDefaultTaxCode, getRateID, getRequestType, getValidWaypoints} from '@libs/TransactionUtils'; import {openDraftWorkspaceRequest} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -55,11 +71,11 @@ function IOURequestStepConfirmation({ const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails(); - const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`); - const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, reportReal)}`); - const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportReal)}`); - const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, reportReal)}`); + const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${getIOURequestPolicyID(transaction, reportDraft)}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getIOURequestPolicyID(transaction, reportReal)}`); + const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getIOURequestPolicyID(transaction, reportReal)}`); + const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getIOURequestPolicyID(transaction, reportDraft)}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getIOURequestPolicyID(transaction, reportReal)}`); const report = reportReal ?? reportDraft; const policy = policyReal ?? policyDraft; @@ -74,7 +90,7 @@ function IOURequestStepConfirmation({ const [selectedParticipantList, setSelectedParticipantList] = useState([]); const [receiptFile, setReceiptFile] = useState>(); - const requestType = TransactionUtils.getRequestType(transaction); + const requestType = getRequestType(transaction); const isDistanceRequest = requestType === CONST.IOU.REQUEST_TYPE.DISTANCE; const isPerDiemRequest = requestType === CONST.IOU.REQUEST_TYPE.PER_DIEM; const [lastLocationPermissionPrompt] = useOnyx(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT); @@ -82,13 +98,13 @@ function IOURequestStepConfirmation({ const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; const receiptType = transaction?.receipt?.type; - const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; - const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction); + const customUnitRateID = getRateID(transaction) ?? ''; + const defaultTaxCode = getDefaultTaxCode(policy, transaction); const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? ''; const transactionTaxAmount = transaction?.taxAmount ?? 0; const isSharingTrackExpense = action === CONST.IOU.ACTION.SHARE; const isCategorizingTrackExpense = action === CONST.IOU.ACTION.CATEGORIZE; - const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); + const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action); const payeePersonalDetails = useMemo(() => { if (personalDetails?.[transaction?.splitPayerAccountIDs?.at(0) ?? -1]) { return personalDetails?.[transaction?.splitPayerAccountIDs?.at(0) ?? -1]; @@ -127,14 +143,14 @@ function IOURequestStepConfirmation({ if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { return participant; } - return participant.accountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participant.accountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant); }) ?? [], [transaction?.participants, personalDetails, iouType], ); const isPolicyExpenseChat = useMemo(() => participants?.some((participant) => participant.isPolicyExpenseChat), [participants]); const formHasBeenSubmitted = useRef(false); - useFetchRoute(transaction, transaction?.comment?.waypoints, action, IOUUtils.shouldUseTransactionDraft(action) ? CONST.TRANSACTION.STATE.DRAFT : CONST.TRANSACTION.STATE.CURRENT); + useFetchRoute(transaction, transaction?.comment?.waypoints, action, shouldUseTransactionDraft(action) ? CONST.TRANSACTION.STATE.DRAFT : CONST.TRANSACTION.STATE.CURRENT); useEffect(() => { const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat); @@ -149,7 +165,7 @@ function IOURequestStepConfirmation({ const defaultBillable = !!policy?.defaultBillable; useEffect(() => { - IOU.setMoneyRequestBillable(transactionID, defaultBillable); + setMoneyRequestBillable(transactionID, defaultBillable); }, [transactionID, defaultBillable]); useEffect(() => { @@ -157,7 +173,7 @@ function IOURequestStepConfirmation({ return; } if (policyCategories?.[transaction.category] && !policyCategories[transaction.category].enabled) { - IOU.setMoneyRequestCategory(transactionID, '', policy?.id); + setMoneyRequestCategory(transactionID, '', policy?.id); } }, [policy?.id, policyCategories, transaction?.category, transactionID]); @@ -168,7 +184,7 @@ function IOURequestStepConfirmation({ if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !!transaction?.category) { return; } - IOU.setMoneyRequestCategory(transactionID, defaultCategory, policy?.id); + setMoneyRequestCategory(transactionID, defaultCategory, policy?.id); // Prevent resetting to default when unselect category // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [transactionID, requestType, defaultCategory, policy?.id]); @@ -191,7 +207,7 @@ function IOURequestStepConfirmation({ Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, transaction?.reportID || reportID, undefined, action)); return; } - IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID, action); + navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID, action); }, [action, isPerDiemRequest, transaction?.participantsAutoAssigned, transaction?.reportID, participantsAutoAssignedFromRoute, requestType, iouType, transactionID, reportID]); const navigateToAddReceipt = useCallback(() => { @@ -204,7 +220,7 @@ function IOURequestStepConfirmation({ // skip this in case user is moving the transaction as the receipt path will be valid in that case useEffect(() => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isLocalFile = FileUtils.isLocalFile(receiptPath); + const isLocalFile = isLocalFileUtil(receiptPath); if (!isLocalFile) { setReceiptFile(transaction?.receipt); @@ -217,11 +233,11 @@ function IOURequestStepConfirmation({ setReceiptFile(receipt); }; - IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); + navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID, action, transaction?.receipt]); const requestMoney = useCallback( - (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: GpsPoint) => { if (!transaction) { return; } @@ -230,7 +246,7 @@ function IOURequestStepConfirmation({ if (!participant) { return; } - IOU.requestMoney({ + requestMoneyFunc({ report, participantParams: { payeeEmail: currentUserPersonalDetails.login, @@ -267,7 +283,7 @@ function IOURequestStepConfirmation({ ); const trackExpense = useCallback( - (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: OnyxEntry, gpsPoints?: IOU.GpsPoint) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: OnyxEntry, gpsPoints?: GpsPoint) => { if (!report || !transaction) { return; } @@ -275,7 +291,7 @@ function IOURequestStepConfirmation({ if (!participant) { return; } - IOU.trackExpense({ + trackExpenseFunc({ report, isDraftPolicy, action, @@ -302,7 +318,7 @@ function IOURequestStepConfirmation({ taxAmount: transactionTaxAmount, billable: transaction.billable, gpsPoints, - validWaypoints: Object.keys(transaction?.comment?.waypoints ?? {}).length ? TransactionUtils.getValidWaypoints(transaction.comment?.waypoints, true) : undefined, + validWaypoints: Object.keys(transaction?.comment?.waypoints ?? {}).length ? getValidWaypoints(transaction.comment?.waypoints, true) : undefined, actionableWhisperReportActionID: transaction.actionableWhisperReportActionID, linkedTrackedExpenseReportAction: transaction.linkedTrackedExpenseReportAction, linkedTrackedExpenseReportID: transaction.linkedTrackedExpenseReportID, @@ -331,7 +347,7 @@ function IOURequestStepConfirmation({ if (!transaction) { return; } - IOU.createDistanceRequest({ + createDistanceRequestFunc({ report, participants: selectedParticipants, currentUserLogin: currentUserPersonalDetails.login, @@ -355,7 +371,7 @@ function IOURequestStepConfirmation({ taxAmount: transactionTaxAmount, customUnitRateID, splitShares: transaction.splitShares, - validWaypoints: TransactionUtils.getValidWaypoints(transaction.comment?.waypoints, true), + validWaypoints: getValidWaypoints(transaction.comment?.waypoints, true), billable: transaction.billable, }, }); @@ -400,7 +416,7 @@ function IOURequestStepConfirmation({ // If we have a receipt let's start the split expense by creating only the action, the transaction, and the group DM if needed if (iouType === CONST.IOU.TYPE.SPLIT && receiptFile) { if (currentUserPersonalDetails.login && !!transaction) { - IOU.startSplitBill({ + startSplitBill({ participants: selectedParticipants, currentUserLogin: currentUserPersonalDetails.login, currentUserAccountID: currentUserPersonalDetails.accountID, @@ -422,7 +438,7 @@ function IOURequestStepConfirmation({ // Since the user is already viewing the report, we don't need to navigate them to the report if (iouType === CONST.IOU.TYPE.SPLIT && !transaction?.isFromGlobalCreate) { if (currentUserPersonalDetails.login && !!transaction) { - IOU.splitBill({ + splitBill({ participants: splitParticipants, currentUserLogin: currentUserPersonalDetails.login, currentUserAccountID: currentUserPersonalDetails.accountID, @@ -448,7 +464,7 @@ function IOURequestStepConfirmation({ // If the split expense is created from the global create menu, we also navigate the user to the group report if (iouType === CONST.IOU.TYPE.SPLIT) { if (currentUserPersonalDetails.login && !!transaction) { - IOU.splitBillAndOpenReport({ + splitBillAndOpenReport({ participants: splitParticipants, currentUserLogin: currentUserPersonalDetails.login, currentUserAccountID: currentUserPersonalDetails.accountID, @@ -471,7 +487,7 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.INVOICE) { - IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile, policy, policyTags, policyCategories); + sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile, policy, policyTags, policyCategories); return; } @@ -575,13 +591,13 @@ function IOURequestStepConfirmation({ if (paymentMethod === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { setIsConfirmed(true); - IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); + sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); return; } if (paymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { setIsConfirmed(true); - IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); + sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); } }, [transaction?.amount, transaction?.comment, transaction?.currency, participants, currentUserPersonalDetails.accountID, report], @@ -589,7 +605,7 @@ function IOURequestStepConfirmation({ const setBillable = useCallback( (billable: boolean) => { - IOU.setMoneyRequestBillable(transactionID, billable); + setMoneyRequestBillable(transactionID, billable); }, [transactionID], ); @@ -622,7 +638,7 @@ function IOURequestStepConfirmation({ return ( @@ -648,7 +664,7 @@ function IOURequestStepConfirmation({ resetPermissionFlow={() => setStartLocationPermissionFlow(false)} onGrant={() => createTransaction(selectedParticipantList, true)} onDeny={() => { - IOU.updateLastLocationPermissionPrompt(); + updateLastLocationPermissionPrompt(); createTransaction(selectedParticipantList, false); }} /> @@ -670,8 +686,8 @@ function IOURequestStepConfirmation({ iouType={iouType} reportID={reportID} isPolicyExpenseChat={isPolicyExpenseChat} - policyID={IOU.getIOURequestPolicyID(transaction, report)} - bankAccountRoute={ReportUtils.getBankAccountRoute(report)} + policyID={getIOURequestPolicyID(transaction, report)} + bankAccountRoute={getBankAccountRoute(report)} iouMerchant={transaction?.merchant} iouCreated={transaction?.created} isDistanceRequest={isDistanceRequest} diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index cae8b46af6bb..feed191c86b9 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -28,22 +28,30 @@ import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; +import { + replaceReceipt, + requestMoney, + setMoneyRequestParticipantsFromReport, + setMoneyRequestReceipt, + startSplitBill, + trackExpense, + updateLastLocationPermissionPrompt, +} from '@libs/actions/IOU'; +import {readFileAsync, resizeImageIfNeeded, showCameraPermissionsAlert, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; import getPhotoSource from '@libs/fileDownload/getPhotoSource'; import getCurrentPosition from '@libs/getCurrentPosition'; import getPlatform from '@libs/getPlatform'; import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath'; -import * as IOUUtils from '@libs/IOUUtils'; +import {shouldStartLocationPermissionFlow as shouldStartLocationPermissionFlowFunc} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getDefaultTaxCode} from '@libs/TransactionUtils'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; -import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -87,7 +95,7 @@ function IOURequestStepScan({ const [pdfFile, setPdfFile] = useState(null); - const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction); + const defaultTaxCode = getDefaultTaxCode(policy, transaction); const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? ''; const transactionTaxAmount = transaction?.taxAmount ?? 0; @@ -98,10 +106,7 @@ function IOURequestStepScan({ return false; } - return ( - !ReportUtils.isArchivedReport(report, reportNameValuePairs) && - !(ReportUtils.isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))) - ); + return !isArchivedReport(report, reportNameValuePairs) && !(isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))); }, [report, skipConfirmation, policy, reportNameValuePairs]); const {translate} = useLocalize(); @@ -114,7 +119,7 @@ function IOURequestStepScan({ setCameraPermissionStatus(status); if (status === RESULTS.BLOCKED) { - FileUtils.showCameraPermissionsAlert(); + showCameraPermissionsAlert(); } }) .catch(() => { @@ -188,7 +193,7 @@ function IOURequestStepScan({ ); const validateReceipt = (file: FileObject) => { - const {fileExtension} = FileUtils.splitExtensionFromFileName(file?.name ?? ''); + const {fileExtension} = splitExtensionFromFileName(file?.name ?? ''); if ( !CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes( fileExtension.toLowerCase() as TupleToUnion, @@ -246,7 +251,7 @@ function IOURequestStepScan({ const createTransaction = useCallback( (receipt: Receipt, participant: Participant) => { if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense({ + trackExpense({ report, isDraftPolicy: false, participantParams: { @@ -264,7 +269,7 @@ function IOURequestStepScan({ }, }); } else { - IOU.requestMoney({ + requestMoney({ report, participantParams: { payeeEmail: currentUserPersonalDetails.login, @@ -301,10 +306,10 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + const selectedParticipants = setMoneyRequestParticipantsFromReport(transactionID, report); const participants = selectedParticipants.map((participant) => { const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID; - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participantAccountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant); }); if (shouldSkipConfirmation) { @@ -313,7 +318,7 @@ function IOURequestStepScan({ receipt.state = CONST.IOU.RECEIPT_STATE.SCANREADY; if (iouType === CONST.IOU.TYPE.SPLIT) { playSound(SOUNDS.DONE); - IOU.startSplitBill({ + startSplitBill({ participants, currentUserLogin: currentUserPersonalDetails?.login ?? '', currentUserAccountID: currentUserPersonalDetails.accountID, @@ -338,7 +343,7 @@ function IOURequestStepScan({ (successData) => { playSound(SOUNDS.DONE); if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense({ + trackExpense({ report, isDraftPolicy: false, participantParams: { @@ -364,7 +369,7 @@ function IOURequestStepScan({ }, }); } else { - IOU.requestMoney({ + requestMoney({ report, participantParams: { payeeEmail: currentUserPersonalDetails.login, @@ -435,7 +440,7 @@ function IOURequestStepScan({ const updateScanAndNavigate = useCallback( (file: FileObject, source: string) => { navigateBack(); - IOU.replaceReceipt(transactionID, file as File, source); + replaceReceipt(transactionID, file as File, source); }, [transactionID], ); @@ -459,12 +464,12 @@ function IOURequestStepScan({ if (Str.isImage(originalFile.name ?? '') && (originalFile?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { setIsLoadingReceipt(true); } - FileUtils.resizeImageIfNeeded(originalFile).then((file) => { + resizeImageIfNeeded(originalFile).then((file) => { setIsLoadingReceipt(false); // Store the receipt on the transaction object in Onyx // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file. // So, let us also save the file type in receipt for later use during blob fetch - IOU.setMoneyRequestReceipt(transactionID, file?.uri ?? '', file.name ?? '', !isEditing, file.type); + setMoneyRequestReceipt(transactionID, file?.uri ?? '', file.name ?? '', !isEditing, file.type); if (isEditing) { updateScanAndNavigate(file, file?.uri ?? ''); @@ -476,7 +481,7 @@ function IOURequestStepScan({ const gpsRequired = transaction?.amount === 0 && iouType !== CONST.IOU.TYPE.SPLIT && file; if (gpsRequired) { - const shouldStartLocationPermissionFlow = IOUUtils.shouldStartLocationPermissionFlow(); + const shouldStartLocationPermissionFlow = shouldStartLocationPermissionFlowFunc(); if (shouldStartLocationPermissionFlow) { setStartLocationPermissionFlow(true); return; @@ -533,9 +538,9 @@ function IOURequestStepScan({ .then((photo: PhotoFile) => { // Store the receipt on the transaction object in Onyx const source = getPhotoSource(photo.path); - IOU.setMoneyRequestReceipt(transactionID, source, photo.path, !isEditing); + setMoneyRequestReceipt(transactionID, source, photo.path, !isEditing); - FileUtils.readFileAsync( + readFileAsync( source, photo.path, (file) => { @@ -548,7 +553,7 @@ function IOURequestStepScan({ setFileSource(source); const gpsRequired = transaction?.amount === 0 && iouType !== CONST.IOU.TYPE.SPLIT && file; if (gpsRequired) { - const shouldStartLocationPermissionFlow = IOUUtils.shouldStartLocationPermissionFlow(); + const shouldStartLocationPermissionFlow = shouldStartLocationPermissionFlowFunc(); if (shouldStartLocationPermissionFlow) { setStartLocationPermissionFlow(true); return; @@ -725,7 +730,7 @@ function IOURequestStepScan({ resetPermissionFlow={() => setStartLocationPermissionFlow(false)} onGrant={() => navigateToConfirmationStep(fileResize, fileSource, true)} onDeny={() => { - IOU.updateLastLocationPermissionPrompt(); + updateLastLocationPermissionPrompt(); navigateToConfirmationStep(fileResize, fileSource, false); }} /> diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 692381d3301f..fb05c6e72348 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -28,21 +28,29 @@ import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; +import { + replaceReceipt, + requestMoney, + setMoneyRequestParticipantsFromReport, + setMoneyRequestReceipt, + startSplitBill, + trackExpense, + updateLastLocationPermissionPrompt, +} from '@libs/actions/IOU'; +import {isMobile, isMobileWebKit} from '@libs/Browser'; +import {base64ToFile, resizeImageIfNeeded, splitExtensionFromFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; -import * as IOUUtils from '@libs/IOUUtils'; +import {shouldStartLocationPermissionFlow as shouldStartLocationPermissionFlowFunc} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getDefaultTaxCode} from '@libs/TransactionUtils'; import ReceiptDropUI from '@pages/iou/ReceiptDropUI'; import StepScreenDragAndDropWrapper from '@pages/iou/request/step/StepScreenDragAndDropWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; -import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -96,7 +104,7 @@ function IOURequestStepScan({ const isTabActive = useIsFocused(); const isEditing = action === CONST.IOU.ACTION.EDIT; - const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction); + const defaultTaxCode = getDefaultTaxCode(policy, transaction); const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? ''; const transactionTaxAmount = transaction?.taxAmount ?? 0; @@ -107,10 +115,7 @@ function IOURequestStepScan({ return false; } - return ( - !ReportUtils.isArchivedReport(report, reportNameValuePairs) && - !(ReportUtils.isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))) - ); + return !isArchivedReport(report, reportNameValuePairs) && !(isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))); }, [report, skipConfirmation, policy, reportNameValuePairs]); /** @@ -118,7 +123,7 @@ function IOURequestStepScan({ * The last deviceId is of regular len camera. */ const requestCameraPermission = useCallback(() => { - if (!Browser.isMobile()) { + if (!isMobile()) { return; } @@ -129,7 +134,7 @@ function IOURequestStepScan({ setCameraPermissionState('granted'); stream.getTracks().forEach((track) => track.stop()); // Only Safari 17+ supports zoom constraint - if (Browser.isMobileWebKit() && stream.getTracks().length > 0) { + if (isMobileWebKit() && stream.getTracks().length > 0) { let deviceId; for (const track of stream.getTracks()) { const setting = track.getSettings(); @@ -170,7 +175,7 @@ function IOURequestStepScan({ }, []); useEffect(() => { - if (!Browser.isMobile() || !isTabActive) { + if (!isMobile() || !isTabActive) { setVideoConstraints(undefined); return; } @@ -209,9 +214,9 @@ function IOURequestStepScan({ }; function validateReceipt(file: FileObject) { - return FileUtils.validateImageForCorruption(file) + return validateImageForCorruption(file) .then(() => { - const {fileExtension} = FileUtils.splitExtensionFromFileName(file?.name ?? ''); + const {fileExtension} = splitExtensionFromFileName(file?.name ?? ''); if ( !CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes( fileExtension.toLowerCase() as TupleToUnion, @@ -271,7 +276,7 @@ function IOURequestStepScan({ const createTransaction = useCallback( (receipt: Receipt, participant: Participant) => { if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense({ + trackExpense({ report, isDraftPolicy: false, participantParams: { @@ -289,7 +294,7 @@ function IOURequestStepScan({ }, }); } else { - IOU.requestMoney({ + requestMoney({ report, participantParams: { payeeEmail: currentUserPersonalDetails.login, @@ -327,10 +332,10 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + const selectedParticipants = setMoneyRequestParticipantsFromReport(transactionID, report); const participants = selectedParticipants.map((participant) => { const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID; - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participantAccountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant); }); if (shouldSkipConfirmation) { @@ -339,7 +344,7 @@ function IOURequestStepScan({ receipt.state = CONST.IOU.RECEIPT_STATE.SCANREADY; if (iouType === CONST.IOU.TYPE.SPLIT) { playSound(SOUNDS.DONE); - IOU.startSplitBill({ + startSplitBill({ participants, currentUserLogin: currentUserPersonalDetails?.login ?? '', currentUserAccountID: currentUserPersonalDetails.accountID, @@ -364,7 +369,7 @@ function IOURequestStepScan({ (successData) => { playSound(SOUNDS.DONE); if (iouType === CONST.IOU.TYPE.TRACK && report) { - IOU.trackExpense({ + trackExpense({ report, isDraftPolicy: false, participantParams: { @@ -390,7 +395,7 @@ function IOURequestStepScan({ }, }); } else { - IOU.requestMoney({ + requestMoney({ report, participantParams: { payeeEmail: currentUserPersonalDetails.login, @@ -460,7 +465,7 @@ function IOURequestStepScan({ const updateScanAndNavigate = useCallback( (file: FileObject, source: string) => { - IOU.replaceReceipt(transactionID, file as File, source); + replaceReceipt(transactionID, file as File, source); navigateBack(); }, [transactionID, navigateBack], @@ -486,12 +491,12 @@ function IOURequestStepScan({ if (Str.isImage(originalFile.name ?? '') && (originalFile?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { setIsLoadingReceipt(true); } - FileUtils.resizeImageIfNeeded(originalFile).then((file) => { + resizeImageIfNeeded(originalFile).then((file) => { setIsLoadingReceipt(false); // Store the receipt on the transaction object in Onyx const source = URL.createObjectURL(file as Blob); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - IOU.setMoneyRequestReceipt(transactionID, source, file.name || '', !isEditing); + setMoneyRequestReceipt(transactionID, source, file.name || '', !isEditing); if (isEditing) { updateScanAndNavigate(file, source); @@ -502,7 +507,7 @@ function IOURequestStepScan({ setFileSource(source); const gpsRequired = transaction?.amount === 0 && iouType !== CONST.IOU.TYPE.SPLIT && file; if (gpsRequired) { - const shouldStartLocationPermissionFlow = IOUUtils.shouldStartLocationPermissionFlow(); + const shouldStartLocationPermissionFlow = shouldStartLocationPermissionFlowFunc(); if (shouldStartLocationPermissionFlow) { setStartLocationPermissionFlow(true); @@ -540,9 +545,9 @@ function IOURequestStepScan({ } const filename = `receipt_${Date.now()}.png`; - const file = FileUtils.base64ToFile(imageBase64 ?? '', filename); + const file = base64ToFile(imageBase64 ?? '', filename); const source = URL.createObjectURL(file); - IOU.setMoneyRequestReceipt(transactionID, source, file.name, !isEditing); + setMoneyRequestReceipt(transactionID, source, file.name, !isEditing); if (isEditing) { updateScanAndNavigate(file, source); @@ -553,7 +558,7 @@ function IOURequestStepScan({ setFileSource(source); const gpsRequired = transaction?.amount === 0 && iouType !== CONST.IOU.TYPE.SPLIT && file; if (gpsRequired) { - const shouldStartLocationPermissionFlow = IOUUtils.shouldStartLocationPermissionFlow(); + const shouldStartLocationPermissionFlow = shouldStartLocationPermissionFlowFunc(); if (shouldStartLocationPermissionFlow) { setStartLocationPermissionFlow(true); return; @@ -792,8 +797,8 @@ function IOURequestStepScan({ {(isDraggingOverWrapper) => ( <> {isLoadingReceipt && } - - {!(isDraggingOver ?? isDraggingOverWrapper) && (Browser.isMobile() ? mobileCameraView() : desktopUploadView())} + + {!(isDraggingOver ?? isDraggingOverWrapper) && (isMobile() ? mobileCameraView() : desktopUploadView())} { const file = e?.dataTransfer?.files[0]; @@ -819,7 +824,7 @@ function IOURequestStepScan({ resetPermissionFlow={() => setStartLocationPermissionFlow(false)} onGrant={() => navigateToConfirmationStep(fileResize, fileSource, true)} onDeny={() => { - IOU.updateLastLocationPermissionPrompt(); + updateLastLocationPermissionPrompt(); navigateToConfirmationStep(fileResize, fileSource, false); }} /> diff --git a/tests/unit/GoogleTagManagerTest.tsx b/tests/unit/GoogleTagManagerTest.tsx index 9a2041634443..327867361081 100644 --- a/tests/unit/GoogleTagManagerTest.tsx +++ b/tests/unit/GoogleTagManagerTest.tsx @@ -1,9 +1,9 @@ import {NavigationContainer} from '@react-navigation/native'; import {render} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; -import * as IOU from '@libs/actions/IOU'; -import * as PaymentMethods from '@libs/actions/PaymentMethods'; -import * as Policy from '@libs/actions/Policy/Policy'; +import {trackExpense} from '@libs/actions/IOU'; +import {addPaymentCard, addSubscriptionPaymentCard} from '@libs/actions/PaymentMethods'; +import {createWorkspace} from '@libs/actions/Policy/Policy'; import GoogleTagManager from '@libs/GoogleTagManager'; import OnboardingModalNavigator from '@libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator'; import CONST from '@src/CONST'; @@ -55,11 +55,11 @@ describe('GoogleTagManagerTest', () => { test('workspace_created', async () => { // When we run the createWorkspace action a few times - Policy.createWorkspace(); + createWorkspace(); await waitForBatchedUpdates(); - Policy.createWorkspace(); + createWorkspace(); await waitForBatchedUpdates(); - Policy.createWorkspace(); + createWorkspace(); // Then we publish a workspace_created event only once expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); @@ -67,7 +67,7 @@ describe('GoogleTagManagerTest', () => { }); test('workspace_created - categorizeTrackedExpense', () => { - IOU.trackExpense({ + trackExpense({ report: {reportID: '123'}, isDraftPolicy: true, action: CONST.IOU.ACTION.CATEGORIZE, @@ -98,7 +98,7 @@ describe('GoogleTagManagerTest', () => { test('paid_adoption - addPaymentCard', () => { // When we add a payment card - PaymentMethods.addPaymentCard(accountID, { + addPaymentCard(accountID, { expirationDate: '2077-10-30', addressZipCode: 'addressZipCode', cardNumber: 'cardNumber', @@ -113,7 +113,7 @@ describe('GoogleTagManagerTest', () => { test('paid_adoption - addSubscriptionPaymentCard', () => { // When we add a payment card - PaymentMethods.addSubscriptionPaymentCard(accountID, { + addSubscriptionPaymentCard(accountID, { cardNumber: 'cardNumber', cardYear: 'cardYear', cardMonth: 'cardMonth', From 24ec2023f88135269d7585a9cc49f587546056cf Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 16 Jan 2025 17:24:29 +0700 Subject: [PATCH 010/201] fix border style --- src/pages/home/report/ReportActionItemSingle.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 853e830323fc..dee7c028160f 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -213,12 +213,12 @@ function ReportActionItemSingle({ ); const getBackgroundColor = () => { - if (isHovered) { - return styles.sidebarLinkActive.backgroundColor; - } if (isActive) { return theme.messageHighlightBG; } + if (isHovered) { + return theme.hoverComponentBG; + } return theme.sidebar; }; const getAvatar = () => { From cbc63c1a690c9bc5555a71108ddc75b7e24c8476 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 21 Jan 2025 18:20:29 +0700 Subject: [PATCH 011/201] fix: toggle distance unit onpress --- .../DistanceRequest/DistanceRequestFooter.tsx | 20 ++++-- src/components/MapView/MapView.tsx | 67 ++++++++++++++++++- .../MapView/MapViewImpl.website.tsx | 31 +++++++-- src/components/MapView/MapViewTypes.ts | 7 +- .../request/step/IOURequestStepDistance.tsx | 1 + src/styles/index.ts | 2 + 6 files changed, 115 insertions(+), 13 deletions(-) diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx index d25037ec808a..56539a998f94 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import DistanceMapView from '@components/DistanceMapView'; @@ -11,10 +11,12 @@ import type {WayPoint} from '@components/MapView/MapViewTypes'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import {getPersonalPolicy, getPolicy} from '@libs/PolicyUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {MapboxAccessToken} from '@src/types/onyx'; +import type {MapboxAccessToken, Policy} from '@src/types/onyx'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -35,16 +37,25 @@ type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & { /** The transaction being interacted with */ transaction: OnyxEntry; + + /** The policy */ + policy: OnyxEntry; }; -function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}: DistanceRequestFooterProps) { +function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage, policy}: DistanceRequestFooterProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const activePolicy = getPolicy(activePolicyID); const numberOfWaypoints = Object.keys(waypoints ?? {}).length; const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => waypoint?.address).length; const lastWaypointIndex = numberOfWaypoints - 1; + const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy); + const policyCurrency = (policy ?? activePolicy)?.outputCurrency ?? getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate; + const {unit} = mileageRate ?? {}; const getMarkerComponent = useCallback( (icon: IconAsset): ReactNode => ( @@ -114,7 +125,8 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig waypoints={waypointMarkers} styleURL={CONST.MAPBOX.STYLE_URL} overlayStyle={styles.mapEditView} - distance={TransactionUtils.getDistanceInMeters(transaction, 'km')} + distanceInMeters={TransactionUtils.getDistanceInMeters(transaction, undefined)} + unit={unit} /> diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 6f1c7aaee458..9e1bfc462153 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -6,9 +6,12 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as UserLocation from '@libs/actions/UserLocation'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; @@ -24,7 +27,7 @@ import responder from './responder'; import utils from './utils'; const MapView = forwardRef( - ({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true}, ref) => { + ({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true, distanceInMeters, unit}, ref) => { const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION); const navigation = useNavigation(); const {isOffline} = useNetwork(); @@ -39,6 +42,25 @@ const MapView = forwardRef( const shouldInitializeCurrentPosition = useRef(true); const [isAccessTokenSet, setIsAccessTokenSet] = useState(false); + const [distanceUnit, setDistanceUnit] = useState(unit); + useEffect(() => { + if (!unit || distanceUnit) { + return; + } + setDistanceUnit(unit); + }, [unit, distanceUnit]); + + const toggleDistanceUnit = useCallback(() => { + setDistanceUnit((currentUnit) => + currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, + ); + }, []); + + const distanceLabelText = useMemo( + () => DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters ?? 0, distanceUnit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS), + [distanceInMeters, distanceUnit], + ); + // Determines if map can be panned to user's detected // location without bothering the user. It will return // false if user has already started dragging the map or @@ -205,6 +227,26 @@ const MapView = forwardRef( const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]); const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]); + const distanceSymbolCoorinate = useMemo(() => { + const length = directionCoordinates?.length; + // If the array is empty, return undefined + if (!length) { + return undefined; + } + + // Find the index of the middle element + const middleIndex = Math.floor(length / 2); + + // Return the middle element + return directionCoordinates.at(middleIndex); + }, [directionCoordinates]); + + console.log('>>>>>>>>>>'); + console.log('>>>>>>>>>>'); + console.log('>>>>>>>>>>'); + console.log('>>>>>>>>>>'); + console.log('>>>>>>>>>>', DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters, distanceUnit)); + return !isOffline && isAccessTokenSet && !!defaultSettings ? ( ( /> )} - {waypoints?.map(({coordinate, markerComponent, id}) => { const MarkerComponent = markerComponent; if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) { @@ -276,6 +317,28 @@ const MapView = forwardRef( })} {!!directionCoordinates && } + {!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && ( + + + + + {distanceLabelText} + + + + + )} {interactive && ( diff --git a/src/components/MapView/MapViewImpl.website.tsx b/src/components/MapView/MapViewImpl.website.tsx index cdeee9b4b7da..de59cae3df76 100644 --- a/src/components/MapView/MapViewImpl.website.tsx +++ b/src/components/MapView/MapViewImpl.website.tsx @@ -12,6 +12,7 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -43,7 +44,8 @@ const MapViewImpl = forwardRef( directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}, interactive = true, - distance, + distanceInMeters, + unit, }, ref, ) => { @@ -51,6 +53,19 @@ const MapViewImpl = forwardRef( const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const [distanceUnit, setDistanceUnit] = useState(unit); + useEffect(() => { + if (!unit || distanceUnit) { + return; + } + setDistanceUnit(unit); + }, [unit, distanceUnit]); + + const toggleDistanceUnit = useCallback(() => { + setDistanceUnit((currentUnit) => + currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, + ); + }, []); const theme = useTheme(); const styles = useThemeStyles(); @@ -273,18 +288,22 @@ const MapViewImpl = forwardRef( )} - {!!distanceSymbolCoorinate && !!distance && ( + {!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && ( - + - {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS)} - {DistanceRequestUtils.getDistanceForDisplayLabel(distance, CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES)} + {DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters, distanceUnit)} - + )} {waypoints?.map(({coordinate, markerComponent, id}) => { diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts index 14104f79779b..2b706cd642a0 100644 --- a/src/components/MapView/MapViewTypes.ts +++ b/src/components/MapView/MapViewTypes.ts @@ -1,5 +1,6 @@ import type {ReactNode} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; +import {Unit} from '@src/types/onyx/Policy'; type MapViewProps = { // Public access token to be used to fetch map data from Mapbox. @@ -23,7 +24,11 @@ type MapViewProps = { // Whether the map is interactable or not interactive?: boolean; - distance?: number; + // Distance displayed on the map in meters. + distanceInMeters?: number; + + // Unit of measurement for distance + unit?: Unit; }; type DirectionProps = { diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 2eba9759027e..920b2b576b2a 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -535,6 +535,7 @@ function IOURequestStepDistance({ waypoints={waypoints} navigateToWaypointEditPage={navigateToWaypointEditPage} transaction={transaction} + policy={policy} /> } /> diff --git a/src/styles/index.ts b/src/styles/index.ts index fc4eeaa9fb65..90c1976f8780 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4007,6 +4007,8 @@ const styles = (theme: ThemeColors) => paddingVertical: 4, borderRadius: 4, textAlign: 'center', + }, + distanceLabelText: { fontSize: 13, fontWeight: FontUtils.fontWeight.bold, color: colors.productLight100, From b1dd8bab93878963bf7d22fad6ac0de779fc569c Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 22 Jan 2025 01:13:22 +0700 Subject: [PATCH 012/201] fix: lint --- src/components/MapView/MapView.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 9e1bfc462153..e4e10b61d3f7 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -6,7 +6,7 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; -import {PressableWithoutFeedback} from '@components/Pressable'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -241,12 +241,6 @@ const MapView = forwardRef( return directionCoordinates.at(middleIndex); }, [directionCoordinates]); - console.log('>>>>>>>>>>'); - console.log('>>>>>>>>>>'); - console.log('>>>>>>>>>>'); - console.log('>>>>>>>>>>'); - console.log('>>>>>>>>>>', DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters, distanceUnit)); - return !isOffline && isAccessTokenSet && !!defaultSettings ? ( ( coordinate={distanceSymbolCoorinate} id="distance-label" key="distance-label" - onTouchStart={toggleDistanceUnit} - onAccessibilityTap={toggleDistanceUnit} > Date: Wed, 22 Jan 2025 01:32:48 +0700 Subject: [PATCH 013/201] fix: lint --- .../DistanceRequest/DistanceRequestFooter.tsx | 28 +++++++------------ src/components/MapView/MapView.tsx | 6 ++-- .../MapView/MapViewImpl.website.tsx | 6 ++-- src/components/MapView/MapViewTypes.ts | 2 +- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx index 56539a998f94..b3e58467f14a 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import DistanceMapView from '@components/DistanceMapView'; @@ -13,22 +13,17 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {getPersonalPolicy, getPolicy} from '@libs/PolicyUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import { getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P } from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {MapboxAccessToken, Policy} from '@src/types/onyx'; +import type {Policy} from '@src/types/onyx'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; const MAX_WAYPOINTS = 25; -type DistanceRequestFooterOnyxProps = { - /** Data about Mapbox token for calling Mapbox API */ - mapboxAccessToken: OnyxEntry; -}; - -type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & { +type DistanceRequestFooterProps = { /** The waypoints for the distance expense */ waypoints?: WaypointCollection; @@ -42,19 +37,20 @@ type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & { policy: OnyxEntry; }; -function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage, policy}: DistanceRequestFooterProps) { +function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPage, policy}: DistanceRequestFooterProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const activePolicy = getPolicy(activePolicyID); + const [mapboxAccessToken] = useOnyx(ONYXKEYS.MAPBOX_ACCESS_TOKEN); const numberOfWaypoints = Object.keys(waypoints ?? {}).length; const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => waypoint?.address).length; const lastWaypointIndex = numberOfWaypoints - 1; const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy); const policyCurrency = (policy ?? activePolicy)?.outputCurrency ?? getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; - const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate; + const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate; const {unit} = mileageRate ?? {}; const getMarkerComponent = useCallback( @@ -77,7 +73,7 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig return; } - const index = TransactionUtils.getWaypointIndex(key); + const index = getWaypointIndex(key); let MarkerComponent: IconAsset; if (index === 0) { MarkerComponent = Expensicons.DotIndicatorUnfilled; @@ -125,7 +121,7 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig waypoints={waypointMarkers} styleURL={CONST.MAPBOX.STYLE_URL} overlayStyle={styles.mapEditView} - distanceInMeters={TransactionUtils.getDistanceInMeters(transaction, undefined)} + distanceInMeters={getDistanceInMeters(transaction, undefined)} unit={unit} /> @@ -135,8 +131,4 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig DistanceRequestFooter.displayName = 'DistanceRequestFooter'; -export default withOnyx({ - mapboxAccessToken: { - key: ONYXKEYS.MAPBOX_ACCESS_TOKEN, - }, -})(DistanceRequestFooter); +export default DistanceRequestFooter; diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index e4e10b61d3f7..ac202c1dc6a0 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -10,7 +10,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as UserLocation from '@libs/actions/UserLocation'; +import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; @@ -72,7 +72,7 @@ const MapView = forwardRef( if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) { return; } - UserLocation.clearUserLocation(); + clearUserLocation(); }, [initialLocation], ); @@ -96,7 +96,7 @@ const MapView = forwardRef( getCurrentPosition((params) => { const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - UserLocation.setUserLocation(currentCoords); + setUserLocation(currentCoords); }, setCurrentPositionToInitialState); }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), ); diff --git a/src/components/MapView/MapViewImpl.website.tsx b/src/components/MapView/MapViewImpl.website.tsx index de59cae3df76..73d7abf8b21a 100644 --- a/src/components/MapView/MapViewImpl.website.tsx +++ b/src/components/MapView/MapViewImpl.website.tsx @@ -20,7 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; -import * as UserLocation from '@userActions/UserLocation'; +import {clearUserLocation, setUserLocation} from '@userActions/UserLocation'; import CONST from '@src/CONST'; import useLocalize from '@src/hooks/useLocalize'; import useNetwork from '@src/hooks/useNetwork'; @@ -91,7 +91,7 @@ const MapViewImpl = forwardRef( if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) { return; } - UserLocation.clearUserLocation(); + clearUserLocation(); }, [initialLocation], ); @@ -115,7 +115,7 @@ const MapViewImpl = forwardRef( getCurrentPosition((params) => { const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - UserLocation.setUserLocation(currentCoords); + setUserLocation(currentCoords); }, setCurrentPositionToInitialState); }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), ); diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts index 2b706cd642a0..11e90a78fffc 100644 --- a/src/components/MapView/MapViewTypes.ts +++ b/src/components/MapView/MapViewTypes.ts @@ -1,6 +1,6 @@ import type {ReactNode} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import {Unit} from '@src/types/onyx/Policy'; +import type {Unit} from '@src/types/onyx/Policy'; type MapViewProps = { // Public access token to be used to fetch map data from Mapbox. From aff2281d380092f44f788b9d0f130cb2785014a2 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 22 Jan 2025 01:41:39 +0700 Subject: [PATCH 014/201] fix: lint --- src/components/DistanceRequest/DistanceRequestFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx index b3e58467f14a..7bf83f3b9b42 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx @@ -13,7 +13,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {getPersonalPolicy, getPolicy} from '@libs/PolicyUtils'; -import { getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P } from '@libs/TransactionUtils'; +import {getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; From 6f7a06f6519cb1b5fbb2ba9e438a19192568b641 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 22 Jan 2025 01:45:20 +0700 Subject: [PATCH 015/201] fix: remove unused change --- src/styles/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 90c1976f8780..383ffc888fa1 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3996,11 +3996,6 @@ const styles = (theme: ThemeColors) => ...wordBreak.breakWord, }, - reportActionComposeTooltipWrapper: { - backgroundColor: theme.tooltipHighlightBG, - paddingVertical: 8, - borderRadius: variables.componentBorderRadiusMedium, - }, distanceLabelWrapper: { backgroundColor: colors.green400, paddingHorizontal: 8, From adc866b68048f7e94f7c5e81089688c2e87e6e36 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:54:00 -0700 Subject: [PATCH 016/201] Update Connect-to-NetSuite.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Resource Update GH: https://github.com/Expensify/Expensify/issues/463218 • Updating the formatting of the article https://help.expensify.com/articles/new-expensify/connections/netsuite/Configure-Netsuite • Mostly following the formatting of the classic article - https://help.expensify.com/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite --- .../netsuite/Connect-to-NetSuite.md | 136 +++++++++--------- 1 file changed, 67 insertions(+), 69 deletions(-) diff --git a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md b/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md index 990217523743..cf756fca613b 100644 --- a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md +++ b/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md @@ -1,6 +1,6 @@ --- title: Connect to NetSuite -description: Integrate NetSuite with Expensify +description: Connect NetSuite to New Expensify for streamlined expense reporting and accounting integration. order: 1 --- @@ -10,7 +10,7 @@ To use the NetSuite connection, you must have a NetSuite account and an Expensif Expensify’s integration with NetSuite supports syncing data between the two systems. Before you start connecting Expensify with NetSuite, there are a few things to note: -- You must use NetSuite administrator credentials to initiate the connection. +- You must be able to login to NetSuite as an administrator to initiate the connection - A Control Plan in Expensify is required to integrate with NetSuite. - Employees don’t need NetSuite access or a NetSuite license to submit expense reports and sync them to NetSuite. - Each NetSuite subsidiary must be connected to a separate Expensify workspace. @@ -18,38 +18,40 @@ Expensify’s integration with NetSuite supports syncing data between the two sy # Step 1: Install the Expensify Bundle in NetSuite -While logged into NetSuite as an administrator, go to **Customization > SuiteBundler > Search & Install Bundles**, then search for “Expensify”. -Click on the Expensify Connect bundle (Bundle ID 283395). -Click **Install**. -If you already have the Expensify Connect bundle installed, head to **Customization > SuiteBundler > Search & Install Bundles > List**, and update it to the latest version. -Select "Show on Existing Custom Forms" for all available fields. +1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for “Expensify”. +2. Click on the Expensify Connect bundle (Bundle ID 283395). +3. Click **Install**. +4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_, and update it to the latest version. +5. Select **Show on Existing Custom Forms** for all available fields. + # Step 2: Enable Token-Based Authentication -In NetSuite, go to **Setup > Company > Enable Features > SuiteCloud > Manage Authentication**. -Make sure “Token Based Authentication” is enabled. -Click **Save**. +1. In NetSuite, go to _Setup > Company > Enable Features > SuiteCloud > Manage Authentication_. +2. Make sure “Token Based Authentication” is enabled. +3. Click **Save**. + # Step 3: Add Expensify Integration Role to a User -In NetSuite, head to **Lists > Employees**, and find the user to who you would like to add the Expensify Integration role. The user you select must at least have access to the permissions included in the Expensify Integration Role, and Admin access works too, but Admin access is not required. -Click **Edit > Access**, then find the Expensify Integration role in the dropdown and add it to the user. -Click **Save**. +1. In NetSuite, head to Lists > Employees, and find the user to who you would like to add the Expensify Integration role. The user you select must at least have access to the permissions included in the Expensify Integration Role, and Admin access works too, but Admin access is not required. +2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user. +3. Click **Save**. {% include info.html %} Remember that Tokens are linked to a **User** and a **Role**, not solely to a User. It’s important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you’ve initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions. {% include end-info.html %} -# Step 4: Create Access Tokens +# Step 4: Create Access Tokens -In NetSuite, enter “page: tokens” in the Global Search. -Click **New Access Token**. -Select Expensify as the application (this must be the original Expensify integration from the bundle). -Select the role Expensify Integration. -Click **Save**. -Copy and paste the token and token ID to a saved location on your computer (this is the only time you will see these details.) +1. In NetSuite, enter “page: tokens” in the Global Search. +2. Click **New Access Token**. +3. Select Expensify as the application (this must be the original Expensify integration from the bundle). +4. Select the role Expensify Integration. +5. Click **Save**. +6. Copy and paste the token and token ID to a saved location on your computer (this is the only time you will see these details.) # Step 5: Confirm Expense Reports are enabled in NetSuite @@ -59,9 +61,9 @@ Expense Reports must be enabled in order to use Expensify’s integration with N {% include end-info.html %} -In NetSuite, go to **Setup > Company > Enable Features > Employees**. -Confirm the checkbox next to "Expense Reports" is checked. -If not, click the checkbox and then click **Save** to enable Expense Reports. +1. In NetSuite, go to Setup > Company > Enable Features > Employees. +2. Confirm the checkbox next to Expense Reports_ is checked. +3. If not, click the checkbox and then click **Save** to enable Expense Reports. # Step 6: Confirm Expense Categories are set up in NetSuite @@ -70,79 +72,75 @@ If not, click the checkbox and then click **Save** to enable Expense Reports. Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are synced to Expensify as Categories. Each Expense Category is an alias mapped to a General Ledger account so that employees can more easily categorize expenses. {% include end-info.html %} +1. In NetSuite, go to _Setup > Accounting > Expense Categories_ (a list of Expense Categories should show.) +2. If no Expense Categories are visible, click **New** to create new ones. -In NetSuite, go to **Setup > Accounting > Expense Categories** (a list of Expense Categories should show.) -If no Expense Categories are visible, click **New** to create new ones. # Step 7: Confirm Journal Entry Transaction Forms are Configured Properly -In NetSuite, go to **Customization > Forms > Transaction Forms.** -Click **Customize** or **Edit** next to the Standard Journal Entry form. -Click **Screen Fields > Main**. Please verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal." -Click the sub-header **Lines** and verify that the “Show” column for “Receipt URL” is checked. -Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the journal type have this same configuration. +1. In NetSuite, go to _Customization > Forms > Transaction Forms_. +2. Click **Customize** or **Edit** next to the Standard Journal Entry form. +3. Click _Screen Fields > Main_. Please verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal." +4. Click the sub-header **Lines** and verify that the “Show” column for “Receipt URL” is checked. +5. Go to _Customization > Forms > Transaction Forms_ and ensure that all other transaction forms with the journal type have this same configuration. # Step 8: Confirm Expense Report Transaction Forms are Configured Properly - -In NetSuite, go to **Customization > Forms > Transaction Forms.** -Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main.** -Verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal." -Click the second sub-header, **Expenses**, and verify that the "Show" column for "Receipt URL" is checked. -Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the expense report type have this same configuration. +1. In NetSuite, go to _Customization > Forms > Transaction Forms_. +2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click _Screen Fields > Main_. +3. Verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal." +4. Click the second sub-header, **Expenses**, and verify that the "Show" column for "Receipt URL" is checked. +5. Go to _Customization > Forms > Transaction Forms_ and ensure that all other transaction forms with the expense report type have this same configuration. # Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly - -In NetSuite, go to **Customization > Forms > Transaction Forms.** -Click **Customize** or **Edit** next to your preferred Vendor Bill form. -Click **Screen Fields > Main** and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked. -Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class. -Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the vendor bill type have this same configuration. +1. In NetSuite, go to _Customization > Forms > Transaction Forms_. +2. Click **Customize** or **Edit** next to your preferred Vendor Bill form. +3. Click _Screen Fields > Main_ and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked. +4. Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class. +5. Go to _Customization > Forms > Transaction Forms_ and ensure that all other transaction forms with the vendor bill type have this same configuration. # Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly - -In NetSuite, go to **Customization > Forms > Transaction Forms**. -Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click **Screen Fields > Main** and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked. -Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class. -Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the vendor credit type have this same configuration. +1. In NetSuite, go to _Customization > Forms > Transaction Forms_. +2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click _Screen Fields > Main_ and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked. +3. Under the Expenses sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class. +4. Go to _Customization > Forms > Transaction Forms_ and ensure that all other transaction forms with the vendor credit type have this same configuration. # Step 11: Set up Tax Groups (only applicable if tracking taxes) {% include info.html %} **Things to note about tax.** -Expensify imports NetSuite Tax Groups (not Tax Codes). To ensure Tax Groups can be applied to expenses go to **Setup > Accounting > Set Up Taxes** and set the _Tax Code Lists Include_ preference to “Tax Groups And Tax Codes” or “Tax Groups Only.” If this field does not display, it’s not needed for that specific country. +Expensify imports NetSuite Tax Groups (not Tax Codes). To ensure Tax Groups can be applied to expenses go to _Setup > Accounting > Set Up Taxes_ and set the _Tax Code Lists Include_ preference to “Tax Groups And Tax Codes” or “Tax Groups Only.” If this field does not display, it’s not needed for that specific country. Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify. {% include end-info.html %} -Go to **Setup > Accounting > Tax Groups**. -Click **New**. -Select the country for your Tax Group. -Enter the Tax Name (this is what employees will see in Expensify.) -Select the subsidiary for this Tax Group. -Select the Tax Code from the table you wish to include in this Tax Group. -Click **Add**. -Click **Save**. -Create one NetSuite Tax Group for each tax rate you want to show in Expensify. +1. Go to _Setup > Accounting > Tax Groups_. +2. Click **New**. +3. Select the country for your Tax Group. +4. Enter the Tax Name (this is what employees will see in Expensify.) +5. Select the subsidiary for this Tax Group. +6. Select the Tax Code from the table you wish to include in this Tax Group. +7. Click **Add**. +8. Click **Save**. +9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify. # Step 12: Connect Expensify to NetSuite -Click your profile image or icon in the bottom left menu. -Scroll down and click **Workspaces** in the left menu. -Select the workspace you want to connect to NetSuite. -Click **More features** in the left menu. -Click **More features** in the left menu. -Scroll down to the Integrate section and enable the Accounting toggle. -Click **Accounting** in the left menu. -Click **Connect** next to NetSuite. -Click **Next** until you reach setup step 5 (If you followed the instructions above, then the first four setup steps will already be complete.) -On setup step 5, enter your NetSuite Account ID, Token ID, and Token Secret (the NetSuite Account ID can be found in NetSuite by going to **Setup > Integration > Web Services Preferences**.) -Click **Confirm** to complete the setup. +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to connect to NetSuite. +4. Click **More features** in the left menu. +5. Scroll down to the Integrate section and enable the **Accounting** toggle. +6. Click **Accounting** in the left menu. +7. Click **Connect** next to NetSuite. +8. Click **Next** until you reach setup step 5 (If you followed the instructions above, then the first four setup steps will already be complete.) +9. On setup step 5, enter your NetSuite Account ID, Token ID, and Token Secret (the NetSuite Account ID can be found in NetSuite by going to Setup > Integration > Web Services Preferences_.) +10. Click **Confirm** to complete the setup. ![The New Expensify workspace setting is open and the More Features tab is selected and visible. The toggle to enable Accounting is highlighted with an orange call out and is currently in the grey disabled position.]({{site.url}}/assets/images/ExpensifyHelp-Xero-1.png) @@ -151,7 +149,7 @@ Click **Confirm** to complete the setup. After completing the setup, the NetSuite connection will sync. It can take 1-2 minutes to sync with NetSuite. -Once connected, all newly approved and paid reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration). +Once connected, all newly approved and paid reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration). You can then move forward with [configuring the NetSuite settings](https://help.expensify.com/articles/new-expensify/connections/netsuite/Configure-Netsuite) in Expensify. {% include faq-begin.md %} From af5b6fb00059cf55c860393dc9d1527dd0348ba0 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:01:22 -0700 Subject: [PATCH 017/201] Update Connect-to-NetSuite.md Capitalising the title to match https://help.expensify.com/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite --- .../new-expensify/connections/netsuite/Connect-to-NetSuite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md b/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md index cf756fca613b..d2489315f88c 100644 --- a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md +++ b/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md @@ -1,5 +1,5 @@ --- -title: Connect to NetSuite +title: Connect To NetSuite description: Connect NetSuite to New Expensify for streamlined expense reporting and accounting integration. order: 1 --- From e4ece8748689911cd93ea9ca12026f29ea79e2f8 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:09:09 -0700 Subject: [PATCH 018/201] Rename Connect-to-NetSuite.md to Connect-To-NetSuite.md Updating the title of the article with a capital "T" so it matches the title of this article https://help.expensify.com/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite --- .../netsuite/{Connect-to-NetSuite.md => Connect-To-NetSuite.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/new-expensify/connections/netsuite/{Connect-to-NetSuite.md => Connect-To-NetSuite.md} (100%) diff --git a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md b/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md similarity index 100% rename from docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md rename to docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md From dd428f5b0788f906986ab64bff105b34825ba8f8 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:12:35 -0700 Subject: [PATCH 019/201] Update redirects.csv Creating a redirect for https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite to https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-To-NetSuite --- docs/redirects.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 40e8b8d0ca61..c95e201877ef 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -620,3 +620,4 @@ https://help.expensify.com/articles/expensify-classic/connect-credit-cards/compa https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Reconcile-Company-Card-Expenses/ https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards/ https://help.expensify.com/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction,https://help.expensify.com/articles/new-expensify/expensify-card/Disputing-Expensify-Card-Transactions +https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-To-NetSuite From 9c994785dafb0135f3a407dbacd858d1d0fc5ca5 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:25:00 -0700 Subject: [PATCH 020/201] Update Connect-To-NetSuite.md Updated some minor formatting --- .../connections/netsuite/Connect-To-NetSuite.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md b/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md index d2489315f88c..99a67a577500 100644 --- a/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md +++ b/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md @@ -10,7 +10,7 @@ To use the NetSuite connection, you must have a NetSuite account and an Expensif Expensify’s integration with NetSuite supports syncing data between the two systems. Before you start connecting Expensify with NetSuite, there are a few things to note: -- You must be able to login to NetSuite as an administrator to initiate the connection +- You must be able to login to NetSuite as an administrator to initiate the connection. - A Control Plan in Expensify is required to integrate with NetSuite. - Employees don’t need NetSuite access or a NetSuite license to submit expense reports and sync them to NetSuite. - Each NetSuite subsidiary must be connected to a separate Expensify workspace. @@ -18,7 +18,7 @@ Expensify’s integration with NetSuite supports syncing data between the two sy # Step 1: Install the Expensify Bundle in NetSuite -1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for “Expensify”. +1. While logged into NetSuite as an administrator, go to _Customization > SuiteBundler > Search & Install Bundles_, then search for “Expensify”. 2. Click on the Expensify Connect bundle (Bundle ID 283395). 3. Click **Install**. 4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_, and update it to the latest version. @@ -34,7 +34,7 @@ Expensify’s integration with NetSuite supports syncing data between the two sy # Step 3: Add Expensify Integration Role to a User -1. In NetSuite, head to Lists > Employees, and find the user to who you would like to add the Expensify Integration role. The user you select must at least have access to the permissions included in the Expensify Integration Role, and Admin access works too, but Admin access is not required. +1. In NetSuite, head to _Lists > Employees_, and find the user to who you would like to add the Expensify Integration role. The user you select must have access to at least the permissions included in the Expensify Integration Role, but they’re not required to be a NetSuite admin. 2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user. 3. Click **Save**. @@ -61,8 +61,8 @@ Expense Reports must be enabled in order to use Expensify’s integration with N {% include end-info.html %} -1. In NetSuite, go to Setup > Company > Enable Features > Employees. -2. Confirm the checkbox next to Expense Reports_ is checked. +1. In NetSuite, go to _Setup > Company > Enable Features > Employees_. +2. Confirm the checkbox next to Expense Reports is checked. 3. If not, click the checkbox and then click **Save** to enable Expense Reports. From 8e0e08136215bda0795c60c93cdec0660bcea0ec Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 24 Jan 2025 15:14:56 +0700 Subject: [PATCH 021/201] fix background color when hover --- src/components/MultipleAvatars.tsx | 4 ++-- src/pages/home/report/PureReportActionItem.tsx | 12 +++++++++--- src/pages/home/report/ReportActionItemThread.tsx | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 6ce785fbdc73..158970d1e9f3 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -5,7 +5,7 @@ import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getUserDetailTooltipText} from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -114,7 +114,7 @@ function MultipleAvatars({ let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]); - const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => ReportUtils.getUserDetailTooltipText(Number(icon.id), icon.name)) : ['']), [shouldShowTooltip, icons]); + const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => getUserDetailTooltipText(Number(icon.id), icon.name)) : ['']), [shouldShowTooltip, icons]); const avatarSize = useMemo(() => { if (isFocusMode) { diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 2e7c52242cfc..eecde01ec925 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -371,6 +371,7 @@ function PureReportActionItem({ const downloadedPreviews = useRef([]); const prevDraftMessage = usePrevious(draftMessage); const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; + const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked); const isActionableWhisper = isActionableMentionWhisper(action) || isActionableTrackExpense(action) || isActionableReportMentionWhisper(action); const highlightedBackgroundColorIfNeeded = useMemo( @@ -410,7 +411,6 @@ function PureReportActionItem({ } clearAllRelatedReportActionErrors(reportID, action); }; - useEffect( () => () => { // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, @@ -1012,7 +1012,7 @@ function PureReportActionItem({ isHovered={hovered} icons={getIconsForParticipants(oldestFourAccountIDs, personalDetails)} onSecondaryInteraction={showPopover} - isActive={!!isReportActionLinked} + isActive={isReportActionActive} /> )} @@ -1045,7 +1045,7 @@ function PureReportActionItem({ report={report} iouReport={iouReport} isHovered={hovered} - isActive={!!isReportActionLinked} + isActive={isReportActionActive} hasBeenFlagged={ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && !isPendingRemove(action) } @@ -1131,6 +1131,12 @@ function PureReportActionItem({ shouldHandleScroll isDisabled={draftMessage !== undefined} shouldFreezeCapture={isPaymentMethodPopoverActive} + onHoverIn={() => { + setIsReportActionActive(false); + }} + onHoverOut={() => { + setIsReportActionActive(!!isReportActionLinked); + }} > {(hovered) => ( diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index b3a39da997dc..223c2713ac70 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -6,9 +6,9 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {navigateToAndOpenChildReport} from '@libs/actions/Report'; import Timing from '@libs/actions/Timing'; import Performance from '@libs/Performance'; -import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {Icon} from '@src/types/onyx/OnyxCommon'; @@ -51,7 +51,7 @@ function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childR onPress={() => { Performance.markStart(CONST.TIMING.OPEN_REPORT_THREAD); Timing.start(CONST.TIMING.OPEN_REPORT_THREAD); - Report.navigateToAndOpenChildReport(childReportID); + navigateToAndOpenChildReport(childReportID); }} role={CONST.ROLE.BUTTON} accessibilityLabel={`${numberOfReplies} ${replyText}`} From 0cbc740518eb75f74f7ec9820be4c3507e8bfcae Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 24 Jan 2025 13:31:14 +0100 Subject: [PATCH 022/201] start moving logic from getValidOptions to getValidReports --- src/libs/OptionsListUtils.ts | 76 +++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index dc2e159eb89f..e860204cf77c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -193,15 +193,22 @@ type GetValidReportsConfig = { includeInvoiceRooms?: boolean; includeDomainEmail?: boolean; loginsToExclude?: Record; + shouldSeparateWorkspaceChat?: boolean; + shouldSeparateSelfDMChat?: boolean; } & GetValidOptionsSharedConfig; +type GetValidReportsReturnTypeCombined = { + // selfDMOptions: OptionData[]; + selfDMOption: OptionData | undefined; + workspaceOptions: OptionData[]; + recentReports: OptionData[]; +}; + type GetOptionsConfig = { excludeLogins?: Record; includeRecentReports?: boolean; includeSelectedOptions?: boolean; recentAttendees?: Attendee[]; - shouldSeparateWorkspaceChat?: boolean; - shouldSeparateSelfDMChat?: boolean; } & GetValidReportsConfig; type GetUserToInviteConfig = { @@ -1244,7 +1251,26 @@ function getUserToInviteOption({ function getValidReports( reports: OptionList['reports'], - { + config: GetValidReportsConfig & {shouldSeparateSelfDMChat: true; shouldSeparateWorkspaceChat: true}, +): GetValidReportsReturnTypeCombined; + +function getValidReports( + reports: OptionList['reports'], + config: GetValidReportsConfig & {shouldSeparateSelfDMChat: true; shouldSeparateWorkspaceChat?: false}, +): Omit; + +function getValidReports( + reports: OptionList['reports'], + config: GetValidReportsConfig & {shouldSeparateSelfDMChat?: false; shouldSeparateWorkspaceChat: true}, +): Omit; + +function getValidReports(reports: OptionList['reports'], config: GetValidReportsConfig & {shouldSeparateSelfDMChat?: false; shouldSeparateWorkspaceChat?: false}): OptionData[]; + +function getValidReports( + reports: OptionList['reports'], + config: GetValidReportsConfig, +): GetValidReportsReturnTypeCombined | Omit | Omit | OptionData[] { + const { betas = [], includeMultipleParticipantReports = false, showChatPreviewLine = false, @@ -1261,12 +1287,15 @@ function getValidReports( includeP2P = true, includeDomainEmail = false, shouldBoldTitleByDefault = true, - loginsToExclude = {}, - }: GetValidReportsConfig, -) { + loginsToExclude = [], + shouldSeparateSelfDMChat, + shouldSeparateWorkspaceChat, + } = config; const topmostReportId = Navigation.getTopmostReportId(); const validReportOptions: OptionData[] = []; + let workspaceChats: OptionData[] = []; + let selfDMChat: OptionData | undefined; const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE; for (let i = 0; i < reports.length; i++) { @@ -1399,6 +1428,41 @@ function getValidReports( }; validReportOptions.push(newReportOption); + + if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) { + workspaceChats.push(newReportOption); + } + + // if (shouldSeparateWorkspaceChat) { + // recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat); + // } + + if (shouldSeparateSelfDMChat && newReportOption.isSelfDM) { + selfDMChat = newReportOption; + // recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM); + } + } + + if (shouldSeparateSelfDMChat && shouldSeparateWorkspaceChat) { + return { + recentReports: validReportOptions, + workspaceOptions: workspaceChats, + selfDMOption: selfDMChat, + }; + } + + if (shouldSeparateSelfDMChat) { + return { + recentReports: validReportOptions, + selfDMOption: selfDMChat, + }; + } + + if (shouldSeparateWorkspaceChat) { + return { + recentReports: validReportOptions, + workspaceOptions: workspaceChats, + }; } return validReportOptions; From bbcb090cdad4c1e9cd9ff5d93e7e5554493a919b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 24 Jan 2025 16:38:25 +0100 Subject: [PATCH 023/201] move loops from getValidOptions to getValidReports --- src/libs/OptionsListUtils.ts | 98 ++++++++++-------------------------- 1 file changed, 27 insertions(+), 71 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e860204cf77c..e685df01c1ec 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1249,27 +1249,7 @@ function getUserToInviteOption({ return userToInvite; } -function getValidReports( - reports: OptionList['reports'], - config: GetValidReportsConfig & {shouldSeparateSelfDMChat: true; shouldSeparateWorkspaceChat: true}, -): GetValidReportsReturnTypeCombined; - -function getValidReports( - reports: OptionList['reports'], - config: GetValidReportsConfig & {shouldSeparateSelfDMChat: true; shouldSeparateWorkspaceChat?: false}, -): Omit; - -function getValidReports( - reports: OptionList['reports'], - config: GetValidReportsConfig & {shouldSeparateSelfDMChat?: false; shouldSeparateWorkspaceChat: true}, -): Omit; - -function getValidReports(reports: OptionList['reports'], config: GetValidReportsConfig & {shouldSeparateSelfDMChat?: false; shouldSeparateWorkspaceChat?: false}): OptionData[]; - -function getValidReports( - reports: OptionList['reports'], - config: GetValidReportsConfig, -): GetValidReportsReturnTypeCombined | Omit | Omit | OptionData[] { +function getValidReports(reports: OptionList['reports'], config: GetValidReportsConfig): GetValidReportsReturnTypeCombined { const { betas = [], includeMultipleParticipantReports = false, @@ -1287,14 +1267,14 @@ function getValidReports( includeP2P = true, includeDomainEmail = false, shouldBoldTitleByDefault = true, - loginsToExclude = [], + loginsToExclude = {}, shouldSeparateSelfDMChat, shouldSeparateWorkspaceChat, } = config; const topmostReportId = Navigation.getTopmostReportId(); const validReportOptions: OptionData[] = []; - let workspaceChats: OptionData[] = []; + const workspaceChats: OptionData[] = []; let selfDMChat: OptionData | undefined; const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE; @@ -1427,45 +1407,30 @@ function getValidReports( lastIOUCreationDate, }; - validReportOptions.push(newReportOption); + // validReportOptions.push(newReportOption); - if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) { - workspaceChats.push(newReportOption); - } + // if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) { + // workspaceChats.push(newReportOption); + // } - // if (shouldSeparateWorkspaceChat) { - // recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat); + // if (shouldSeparateSelfDMChat && newReportOption.isSelfDM) { + // selfDMChat = newReportOption; // } - if (shouldSeparateSelfDMChat && newReportOption.isSelfDM) { + if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) { + workspaceChats.push(newReportOption); + } else if (shouldSeparateSelfDMChat && newReportOption.isSelfDM) { selfDMChat = newReportOption; - // recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM); + } else { + validReportOptions.push(newReportOption); } } - if (shouldSeparateSelfDMChat && shouldSeparateWorkspaceChat) { - return { - recentReports: validReportOptions, - workspaceOptions: workspaceChats, - selfDMOption: selfDMChat, - }; - } - - if (shouldSeparateSelfDMChat) { - return { - recentReports: validReportOptions, - selfDMOption: selfDMChat, - }; - } - - if (shouldSeparateWorkspaceChat) { - return { - recentReports: validReportOptions, - workspaceOptions: workspaceChats, - }; - } - - return validReportOptions; + return { + recentReports: validReportOptions, + workspaceOptions: workspaceChats, + selfDMOption: selfDMChat, + }; } /** @@ -1504,15 +1469,22 @@ function getValidOptions( // Get valid recent reports: let recentReportOptions: OptionData[] = []; + let workspaceChats: OptionData[] = []; + let selfDMChat: OptionData | undefined; if (includeRecentReports) { - recentReportOptions = getValidReports(options.reports, { + const {recentReports, workspaceOptions, selfDMOption} = getValidReports(options.reports, { ...getValidReportsConfig, includeP2P, includeDomainEmail, selectedOptions, loginsToExclude, shouldBoldTitleByDefault, + shouldSeparateSelfDMChat, + shouldSeparateWorkspaceChat, }); + recentReportOptions = recentReports; + workspaceChats = workspaceOptions; + selfDMChat = selfDMOption; } else if (recentAttendees && recentAttendees?.length > 0) { recentAttendees.filter((attendee) => { const login = attendee.login ?? attendee.displayName; @@ -1558,22 +1530,6 @@ function getValidOptions( } } - let workspaceChats: OptionData[] = []; - - if (shouldSeparateWorkspaceChat) { - workspaceChats = recentReportOptions.filter((option) => option.isOwnPolicyExpenseChat && !option.private_isArchived); - } - - let selfDMChat: OptionData | undefined; - - if (shouldSeparateWorkspaceChat) { - recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat); - } - if (shouldSeparateSelfDMChat) { - selfDMChat = recentReportOptions.find((option) => option.isSelfDM); - recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM); - } - return { personalDetails: personalDetailsOptions, recentReports: recentReportOptions, From 4e4e6a818de50912831e22bfa9707037aec337d5 Mon Sep 17 00:00:00 2001 From: shahid Date: Fri, 24 Jan 2025 21:34:06 +0530 Subject: [PATCH 024/201] Update the Export icon in report details page --- assets/images/export.svg | 6 ++++++ src/components/Icon/Expensicons.ts | 2 ++ src/pages/ReportDetailsPage.tsx | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 assets/images/export.svg diff --git a/assets/images/export.svg b/assets/images/export.svg new file mode 100644 index 000000000000..ed6ae9897368 --- /dev/null +++ b/assets/images/export.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index e4072504f3d6..1f9b8c1a01e7 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -72,6 +72,7 @@ import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg'; import DotIndicator from '@assets/images/dot-indicator.svg'; import DownArrow from '@assets/images/down.svg'; import Download from '@assets/images/download.svg'; +import Export from '@assets/images/export.svg'; import DragAndDrop from '@assets/images/drag-and-drop.svg'; import DragHandles from '@assets/images/drag-handles.svg'; import Emoji from '@assets/images/emoji.svg'; @@ -269,6 +270,7 @@ export { DotIndicatorUnfilled, DownArrow, Download, + Export, DragAndDrop, DragHandles, EReceiptIcon, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index fc112cb09ba3..5aa3c1c71a61 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -553,7 +553,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT, translationKey: 'common.export', - icon: Expensicons.Upload, + icon: Expensicons.Export, isAnonymousAction: false, action: () => { Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID, connectedIntegration, backTo)); From 8a04d7b19f7c7786c0ab0a98d953304cbfc90394 Mon Sep 17 00:00:00 2001 From: shahid Date: Fri, 24 Jan 2025 21:59:13 +0530 Subject: [PATCH 025/201] fix lint --- src/components/Icon/Expensicons.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 1f9b8c1a01e7..0bcfc05ba9c2 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -72,7 +72,6 @@ import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg'; import DotIndicator from '@assets/images/dot-indicator.svg'; import DownArrow from '@assets/images/down.svg'; import Download from '@assets/images/download.svg'; -import Export from '@assets/images/export.svg'; import DragAndDrop from '@assets/images/drag-and-drop.svg'; import DragHandles from '@assets/images/drag-handles.svg'; import Emoji from '@assets/images/emoji.svg'; @@ -89,6 +88,7 @@ import ExpensifyFooterLogoVertical from '@assets/images/expensify-footer-logo-ve import ExpensifyFooterLogo from '@assets/images/expensify-footer-logo.svg'; import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg'; import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg'; +import Export from '@assets/images/export.svg'; import EyeDisabled from '@assets/images/eye-disabled.svg'; import Eye from '@assets/images/eye.svg'; import Feed from '@assets/images/feed.svg'; @@ -270,7 +270,6 @@ export { DotIndicatorUnfilled, DownArrow, Download, - Export, DragAndDrop, DragHandles, EReceiptIcon, @@ -285,6 +284,7 @@ export { ExpensifyFooterLogo, ExpensifyFooterLogoVertical, Expand, + Export, Eye, EyeDisabled, FallbackAvatar, From 400a28ab591890921cc16432229d0dd458329e3d Mon Sep 17 00:00:00 2001 From: Mohit Date: Sat, 25 Jan 2025 12:02:19 +0530 Subject: [PATCH 026/201] Report isn't showing most recent chats until refreshed --- src/components/Search/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 03b6c820da00..27ce4c24dcdb 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -215,7 +215,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo transactions, previousTransactions, queryJSON, - offset, + // Set offset to 0 to retrieve the most recent chat messages. + offset: 0, reportActions, previousReportActions, }); From 45641833f15a494be9ceda4c4c7d301a74a14ed5 Mon Sep 17 00:00:00 2001 From: Mohit Date: Sat, 25 Jan 2025 14:54:10 +0530 Subject: [PATCH 027/201] Fix eslint issues --- src/components/Search/index.tsx | 67 +++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 27ce4c24dcdb..7938fd598304 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -15,16 +15,25 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as SearchActions from '@libs/actions/Search'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {createTransactionThread, search} from '@libs/actions/Search'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import memoize from '@libs/memoize'; import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as SearchQueryUtils from '@libs/SearchQueryUtils'; -import * as SearchUIUtils from '@libs/SearchUIUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {generateReportID} from '@libs/ReportUtils'; +import {buildSearchQueryString} from '@libs/SearchQueryUtils'; +import { + getListItem, + getSections, + getSortedSections, + isReportActionListItemType, + isReportListItemType, + isSearchResultsEmpty as isSearchResultsEmptyFromUiUtils, + isTransactionListItemType, + shouldShowYear as shouldShowYearFromUiUtils, +} from '@libs/SearchUIUtils'; +import {isOnHold} from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; import type {AuthScreensParamList} from '@navigation/types'; import EmptySearchView from '@pages/Search/EmptySearchView'; @@ -57,7 +66,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri isSelected: true, canDelete: item.canDelete, canHold: item.canHold, - isHeld: TransactionUtils.isOnHold(item), + isHeld: isOnHold(item), canUnhold: item.canUnhold, action: item.action, reportID: item.reportID, @@ -77,14 +86,14 @@ function mapToItemWithSelectionInfo( canSelectMultiple: boolean, shouldAnimateInHighlight: boolean, ) { - if (SearchUIUtils.isReportActionListItemType(item)) { + if (isReportActionListItemType(item)) { return { ...item, shouldAnimateInHighlight, }; } - return SearchUIUtils.isTransactionListItemType(item) + return isTransactionListItemType(item) ? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight) : { ...item, @@ -107,7 +116,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact isSelected: true, canDelete: item.canDelete, canHold: item.canHold, - isHeld: TransactionUtils.isOnHold(item), + isHeld: isOnHold(item), canUnhold: item.canUnhold, action: item.action, reportID: item.reportID, @@ -176,12 +185,12 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo return; } - SearchActions.search({queryJSON, offset}); + search({queryJSON, offset}); }, [isOffline, offset, queryJSON]); const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => { - if (SearchUIUtils.isTransactionListItemType(item) || SearchUIUtils.isReportActionListItemType(item)) { + if (isTransactionListItemType(item) || isReportActionListItemType(item)) { return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding; } @@ -230,14 +239,14 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const shouldShowLoadingState = !isOffline && !isDataLoaded; const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0; - const isSearchResultsEmpty = !searchResults?.data || SearchUIUtils.isSearchResultsEmpty(searchResults); + const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyFromUiUtils(searchResults); const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty); const data = useMemo(() => { if (searchResults === undefined) { return []; } - return SearchUIUtils.getSections(type, status, searchResults.data, searchResults.search); + return getSections(type, status, searchResults.data, searchResults.search); }, [searchResults, status, type]); useEffect(() => { @@ -261,7 +270,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo newTransactionList[transaction.transactionID] = { action: transaction.action, canHold: transaction.canHold, - isHeld: TransactionUtils.isOnHold(transaction), + isHeld: isOnHold(transaction), canUnhold: transaction.canUnhold, isSelected: selectedTransactions[transaction.transactionID].isSelected, canDelete: transaction.canDelete, @@ -282,7 +291,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo newTransactionList[transaction.transactionID] = { action: transaction.action, canHold: transaction.canHold, - isHeld: TransactionUtils.isOnHold(transaction), + isHeld: isOnHold(transaction), canUnhold: transaction.canUnhold, isSelected: selectedTransactions[transaction.transactionID].isSelected, canDelete: transaction.canDelete, @@ -329,8 +338,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo return {null}; } - const ListItem = SearchUIUtils.getListItem(type, status); - const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder); + const ListItem = getListItem(type, status); + const sortedData = getSortedSections(type, status, data, sortBy, sortOrder); const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT; const sortedSelectedData = sortedData.map((item) => { const baseKey = isChat @@ -365,10 +374,10 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo } const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => { - if (SearchUIUtils.isReportActionListItemType(item)) { + if (isReportActionListItemType(item)) { return; } - if (SearchUIUtils.isTransactionListItemType(item)) { + if (isTransactionListItemType(item)) { if (!item.keyForList) { return; } @@ -399,21 +408,21 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo const openReport = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => { const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORTID; - let reportID = SearchUIUtils.isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID; + let reportID = isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID; if (!reportID) { return; } // If we're trying to open a legacy transaction without a transaction thread, let's create the thread and navigate the user - if (SearchUIUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) { - reportID = ReportUtils.generateReportID(); - SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID); + if (isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) { + reportID = generateReportID(); + createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID); } const backTo = Navigation.getActiveRoute(); - if (SearchUIUtils.isReportActionListItemType(item)) { + if (isReportActionListItemType(item)) { const reportActionID = item.reportActionID; Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo})); return; @@ -449,11 +458,11 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo }; const onSortPress = (column: SearchColumnType, order: SortOrder) => { - const newQuery = SearchQueryUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order}); + const newQuery = buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order}); navigation.setParams({q: newQuery}); }; - const shouldShowYear = SearchUIUtils.shouldShowYear(searchResults?.data); + const shouldShowYear = shouldShowYearFromUiUtils(searchResults?.data); const shouldShowSorting = !Array.isArray(status) && sortableSearchStatuses.includes(status); return ( @@ -478,7 +487,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo ) } isSelected={(item) => - status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUIUtils.isReportListItemType(item) + status !== CONST.SEARCH.STATUS.EXPENSE.ALL && isReportListItemType(item) ? item.transactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected) : !!item.isSelected } @@ -502,7 +511,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo onSelectRow={openReport} getItemHeight={getItemHeightMemoized} shouldSingleExecuteRowSelect - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} shouldPreventDefault={false} listHeaderWrapperStyle={[styles.ph8, styles.pt3]} containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]} From 9f403a75224ab13c749148a9ebc0d7435e9c6ea3 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 28 Jan 2025 17:19:39 +0700 Subject: [PATCH 028/201] fix: update label color to green500 --- src/styles/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index dd6a4173305e..0001f5b77c3d 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3994,7 +3994,7 @@ const styles = (theme: ThemeColors) => }, distanceLabelWrapper: { - backgroundColor: colors.green400, + backgroundColor: colors.green500, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, From d8d532e4bd1752e542f34e85f78377da2a6ecf0b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 28 Jan 2025 15:17:42 +0100 Subject: [PATCH 029/201] change workspace subtitle --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/params.ts | 5 +++++ src/libs/ReportUtils.ts | 10 +++++++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b55d6fbc75c..fcb5a626c004 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -154,6 +154,7 @@ import type { StatementTitleParams, StepCounterParams, StripePaidParams, + SubmitsToParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -1106,6 +1107,7 @@ const translations = { }), dates: 'Dates', rates: 'Rates', + submitsTo: ({name}: SubmitsToParams) => `Submits to ${name}`, }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index dff3dcd575c0..5dc40ceae4c1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -153,6 +153,7 @@ import type { StatementTitleParams, StepCounterParams, StripePaidParams, + SubmitsToParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -1104,6 +1105,7 @@ const translations = { }), dates: 'Fechas', rates: 'Tasas', + submitsTo: ({name}: SubmitsToParams) => `Se envía a ${name}`, }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/languages/params.ts b/src/languages/params.ts index 9d28f198b704..b111999aebdf 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -600,6 +600,10 @@ type FlightLayoverParams = { layover: string; }; +type SubmitsToParams = { + name: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -813,4 +817,5 @@ export type { ChatWithAccountManagerParams, EditDestinationSubtitleParams, FlightLayoverParams, + SubmitsToParams, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1015d0b5fec6..cea464ffcd37 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4393,7 +4393,15 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { return report?.reportName?.substring(1) ?? ''; } if ((isPolicyExpenseChat(report) && !!report?.isOwnPolicyExpenseChat) || isExpenseReport(report)) { - return translateLocal('workspace.common.workspace'); + const submitToAccountID = getSubmitToAccountID(getPolicy(report?.policyID), report); + const submitsToAccountDetails = allPersonalDetails?.[submitToAccountID]; + const subtitle = submitsToAccountDetails?.displayName ?? submitsToAccountDetails?.login; + + if (!subtitle) { + return translateLocal('workspace.common.workspace'); + } + + return translateLocal('iou.submitsTo', {name: subtitle ?? ''}); } if (isArchivedReport(getReportNameValuePairs(report?.reportID))) { return report?.oldPolicyName ?? ''; From 2136d6ad2834e4085fdd1aa873d81dbd1a0bd3ba Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 28 Jan 2025 20:05:07 +0100 Subject: [PATCH 030/201] remove unnecessary comments --- src/libs/OptionsListUtils.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e685df01c1ec..0be9659f8a3f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -198,7 +198,6 @@ type GetValidReportsConfig = { } & GetValidOptionsSharedConfig; type GetValidReportsReturnTypeCombined = { - // selfDMOptions: OptionData[]; selfDMOption: OptionData | undefined; workspaceOptions: OptionData[]; recentReports: OptionData[]; @@ -1407,16 +1406,6 @@ function getValidReports(reports: OptionList['reports'], config: GetValidReports lastIOUCreationDate, }; - // validReportOptions.push(newReportOption); - - // if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) { - // workspaceChats.push(newReportOption); - // } - - // if (shouldSeparateSelfDMChat && newReportOption.isSelfDM) { - // selfDMChat = newReportOption; - // } - if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) { workspaceChats.push(newReportOption); } else if (shouldSeparateSelfDMChat && newReportOption.isSelfDM) { From 831811d4f501fd3bbf35b3701c03b19211710107 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 29 Jan 2025 16:28:26 +0100 Subject: [PATCH 031/201] change confirmation page subtitle --- src/libs/OptionsListUtils.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index c704b588d0cb..e80b1d221e86 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -40,7 +40,7 @@ import Parser from './Parser'; import Performance from './Performance'; import {getDisplayNameOrDefault} from './PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from './PhoneNumber'; -import {canSendInvoiceFromWorkspace} from './PolicyUtils'; +import {canSendInvoiceFromWorkspace, getSubmitToAccountID} from './PolicyUtils'; import { getCombinedReportActions, getExportIntegrationLastMessageText, @@ -874,6 +874,17 @@ function getReportOption(participant: Participant): OptionData { } else { option.text = getPolicyName(report); option.alternateText = translateLocal('workspace.common.workspace'); + + if (report?.policyID) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const submitToAccountID = getSubmitToAccountID(policy, report); + const submitsToAccountDetails = allPersonalDetails?.[submitToAccountID]; + const subtitle = submitsToAccountDetails?.displayName ?? submitsToAccountDetails?.login; + + if (subtitle) { + option.alternateText = translateLocal('iou.submitsTo', {name: subtitle ?? ''}); + } + } } option.isDisabled = isDraftReport(participant.reportID); option.selected = participant.selected; From b80828cfd655c594b4f4f99f4ce786f17f138e51 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 29 Jan 2025 16:51:34 +0100 Subject: [PATCH 032/201] only show submits to in create expense flow --- src/components/AvatarWithDisplayName.tsx | 2 +- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 98e6dd626883..322097b56230 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -72,7 +72,7 @@ function AvatarWithDisplayName({ `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, ); const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); - const subtitle = ReportUtils.getChatRoomSubtitle(report); + const subtitle = ReportUtils.getChatRoomSubtitle(report, {isCreateExpenseFlow: true}); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e80b1d221e86..9c550de24ddd 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -803,7 +803,7 @@ function createOption( result.tooltipText = getReportParticipantsTitle(visibleParticipantAccountIDs); hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || reportUtilsIsGroupChat(report); - subtitle = getChatRoomSubtitle(report); + subtitle = getChatRoomSubtitle(report, {isCreateExpenseFlow: true}); const lastActorDetails = report.lastActorAccountID ? personalDetailMap[report.lastActorAccountID] : null; const lastActorDisplayName = getLastActorDisplayName(lastActorDetails, hasMultipleParticipants); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cea464ffcd37..9e24f15bbcc0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -692,6 +692,10 @@ type Thread = { parentReportActionID: string; } & Report; +type GetChatRoomSubtitleConfig = { + isCreateExpenseFlow?: boolean; +}; + let currentUserEmail: string | undefined; let currentUserPrivateDomain: string | undefined; let currentUserAccountID: number | undefined; @@ -4372,7 +4376,7 @@ function getPayeeName(report: OnyxEntry): string | undefined { /** * Get either the policyName or domainName the chat is tied to */ -function getChatRoomSubtitle(report: OnyxEntry): string | undefined { +function getChatRoomSubtitle(report: OnyxEntry, config: GetChatRoomSubtitleConfig = {isCreateExpenseFlow: false}): string | undefined { if (isChatThread(report)) { return ''; } @@ -4397,7 +4401,7 @@ function getChatRoomSubtitle(report: OnyxEntry): string | undefined { const submitsToAccountDetails = allPersonalDetails?.[submitToAccountID]; const subtitle = submitsToAccountDetails?.displayName ?? submitsToAccountDetails?.login; - if (!subtitle) { + if (!subtitle || !config.isCreateExpenseFlow) { return translateLocal('workspace.common.workspace'); } From d4dfd05385aea12806ed40d3586b45d437ec7130 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 30 Jan 2025 01:54:33 +0700 Subject: [PATCH 033/201] fix bug --- src/pages/home/report/PureReportActionItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index a87b3fa4346f..5a5eef58b62c 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1013,7 +1013,7 @@ function PureReportActionItem({ isHovered={hovered} icons={getIconsForParticipants(oldestFourAccountIDs, personalDetails)} onSecondaryInteraction={showPopover} - isActive={isReportActionActive} + isActive={isReportActionActive && !isContextMenuActive} /> )} @@ -1046,7 +1046,7 @@ function PureReportActionItem({ report={report} iouReport={iouReport} isHovered={hovered} - isActive={isReportActionActive} + isActive={isReportActionActive && !isContextMenuActive} hasBeenFlagged={ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && !isPendingRemove(action) } From c5cdd198ca57993e909e2a59813377e14145e046 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 30 Jan 2025 11:40:24 +0100 Subject: [PATCH 034/201] Refactor invoice report name generation to include the current date --- src/libs/ReportUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5e50f69e28fa..739147afb9ad 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4914,7 +4914,6 @@ function buildOptimisticInvoiceReport( total: number, currency: string, ): OptimisticExpenseReport { - const formattedTotal = convertToDisplayString(total, currency); const invoiceReport = { reportID: generateReportID(), chatReportID, @@ -4924,7 +4923,7 @@ function buildOptimisticInvoiceReport( managerID: receiverAccountID, currency, // We don’t translate reportName because the server response is always in English - reportName: `${receiverName} owes ${formattedTotal}`, + reportName: `Invoice ${DateUtils.extractDate(new Date().toString())}`, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.OPEN, total, From 06e257093c7359d74db5726817d06954094ce78d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 30 Jan 2025 12:23:59 +0100 Subject: [PATCH 035/201] Add billable to params --- src/libs/API/parameters/CreatePerDiemRequestParams.ts | 1 + src/libs/actions/IOU.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libs/API/parameters/CreatePerDiemRequestParams.ts b/src/libs/API/parameters/CreatePerDiemRequestParams.ts index 61740fbd0a37..3332b71cebe4 100644 --- a/src/libs/API/parameters/CreatePerDiemRequestParams.ts +++ b/src/libs/API/parameters/CreatePerDiemRequestParams.ts @@ -19,6 +19,7 @@ type CreatePerDiemRequestParams = { reportPreviewReportActionID: string; transactionThreadReportID: string; createdReportActionIDForThread: string | undefined; + billable: boolean; }; export default CreatePerDiemRequestParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 3c86ce947742..5d520f663d21 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4489,6 +4489,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf tag, transactionThreadReportID, createdReportActionIDForThread, + billable, }; API.write(WRITE_COMMANDS.CREATE_PER_DIEM_REQUEST, parameters, onyxData); From 23fa4a43dc2c360079d468192a83d9997ed71a39 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 30 Jan 2025 12:26:40 +0100 Subject: [PATCH 036/201] Pass billable --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index a54c882259c8..0b25bafd48ea 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -312,6 +312,7 @@ function IOURequestStepConfirmation({ category: transaction.category, tag: transaction.tag, customUnit: transaction.comment?.customUnit, + billable: transaction.billable, }, }); }, From 4228a75800ab3c5834c9941c31cc211b2d9969d2 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 30 Jan 2025 12:29:22 +0100 Subject: [PATCH 037/201] better params --- src/libs/API/parameters/CreatePerDiemRequestParams.ts | 2 +- src/libs/actions/IOU.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/API/parameters/CreatePerDiemRequestParams.ts b/src/libs/API/parameters/CreatePerDiemRequestParams.ts index 3332b71cebe4..b2fe0b541cc5 100644 --- a/src/libs/API/parameters/CreatePerDiemRequestParams.ts +++ b/src/libs/API/parameters/CreatePerDiemRequestParams.ts @@ -19,7 +19,7 @@ type CreatePerDiemRequestParams = { reportPreviewReportActionID: string; transactionThreadReportID: string; createdReportActionIDForThread: string | undefined; - billable: boolean; + billable?: boolean; }; export default CreatePerDiemRequestParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5d520f663d21..15a79b7bb27e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -333,6 +333,7 @@ type PerDiemExpenseTransactionParams = { tag?: string; created: string; customUnit: TransactionCustomUnit; + billable?: boolean; }; type RequestMoneyPolicyParams = { From 515ef9a669faf0adc989849697f8e3ec6ea01bc1 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 30 Jan 2025 12:53:13 +0100 Subject: [PATCH 038/201] pass billable back correctly --- src/libs/actions/IOU.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 15a79b7bb27e..6c2c553dd722 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -200,6 +200,7 @@ type MoneyRequestInformation = { transactionThreadReportID: string; createdReportActionIDForThread: string | undefined; onyxData: OnyxData; + billable?: boolean; }; type TrackExpenseInformation = { @@ -2787,7 +2788,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI const {parentChatReport, transactionParams, participantParams, policyParams = {}, moneyRequestReportID = ''} = perDiemExpenseInformation; const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams; const {policy, policyCategories, policyTagList} = policyParams; - const {comment = '', currency, created, category, tag, customUnit} = transactionParams; + const {comment = '', currency, created, category, tag, customUnit, billable} = transactionParams; const amount = computePerDiemExpenseAmount(customUnit); const merchant = computePerDiemExpenseMerchant(customUnit, policy); @@ -2862,6 +2863,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI merchant, tag, customUnit, + billable, pendingFields: {subRates: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, }, }); @@ -2981,6 +2983,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI successData, failureData, }, + billable, }; } @@ -4460,6 +4463,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf transactionThreadReportID, createdReportActionIDForThread, onyxData, + billable, } = getPerDiemExpenseInformation({ parentChatReport: currentChatReport, participantParams, From 4ae487d497ec1e6ab1dad7bcdc37010a524328f3 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 30 Jan 2025 14:44:56 +0100 Subject: [PATCH 039/201] fix lint errors --- src/components/AvatarWithDisplayName.tsx | 90 +++++++++++------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 322097b56230..e9b3b555385d 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,18 +1,33 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import { + getChatRoomSubtitle, + getDisplayNamesWithTooltips, + getIcons, + getParentNavigationSubtitle, + getReportName, + isChatThread, + isExpenseReport, + isInvoiceReport, + isIOUReport, + isMoneyRequest, + isMoneyRequestReport, + isTrackExpenseReport, + navigateToDetailsPage, + shouldReportShowSubscript, +} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import CaretWrapper from './CaretWrapper'; import DisplayNames from './DisplayNames'; @@ -23,15 +38,7 @@ import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; -type AvatarWithDisplayNamePropsWithOnyx = { - /** All of the actions of the report */ - parentReportActions: OnyxEntry; - - /** Personal details of all users */ - personalDetails: OnyxEntry; -}; - -type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { +type AvatarWithDisplayNameProps = { /** The report currently being looked at */ report: OnyxEntry; @@ -55,41 +62,38 @@ const fallbackIcon: Icon = { id: -1, }; -function AvatarWithDisplayName({ - policy, - report, - parentReportActions, - isAnonymous = false, - size = CONST.AVATAR_SIZE.DEFAULT, - shouldEnableDetailPageNavigation = false, - personalDetails = CONST.EMPTY_OBJECT, -}: AvatarWithDisplayNameProps) { +function AvatarWithDisplayName({policy, report, isAnonymous = false, size = CONST.AVATAR_SIZE.DEFAULT, shouldEnableDetailPageNavigation = false}: AvatarWithDisplayNameProps) { + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {canEvict: false}); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) ?? CONST.EMPTY_OBJECT; + const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); const [invoiceReceiverPolicy] = useOnyx( - `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, + `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : CONST.DEFAULT_NUMBER_ID}`, ); - const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); - const subtitle = ReportUtils.getChatRoomSubtitle(report, {isCreateExpenseFlow: true}); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); - const isMoneyRequestOrReport = - ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); - const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); - const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); - const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); + const title = getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); + const subtitle = getChatRoomSubtitle(report, {isCreateExpenseFlow: true}); + const parentNavigationSubtitleData = getParentNavigationSubtitle(report); + const isMoneyRequestOrReport = isMoneyRequestReport(report) || isMoneyRequest(report) || isTrackExpenseReport(report) || isInvoiceReport(report); + const icons = getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); + const ownerPersonalDetails = getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); + const displayNamesWithTooltips = getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); + const shouldShowSubscriptAvatar = shouldReportShowSubscript(report); const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG; const actorAccountID = useRef(null); useEffect(() => { - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; - actorAccountID.current = parentReportAction?.actorAccountID ?? -1; + if (!report?.parentReportActionID) { + return; + } + const parentReportAction = parentReportActions?.[report?.parentReportActionID]; + actorAccountID.current = parentReportAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID; }, [parentReportActions, report]); const goToDetailsPage = useCallback(() => { - ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute()); + navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute()); }, [report]); const showActorDetails = useCallback(() => { @@ -99,17 +103,17 @@ function AvatarWithDisplayName({ return; } - if (ReportUtils.isExpenseReport(report) && report?.ownerAccountID) { + if (isExpenseReport(report) && report?.ownerAccountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(report.ownerAccountID)); return; } - if (ReportUtils.isIOUReport(report) && report?.reportID) { + if (isIOUReport(report) && report?.reportID) { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report.reportID)); return; } - if (ReportUtils.isChatThread(report)) { + if (isChatThread(report)) { // In an ideal situation account ID won't be 0 if (actorAccountID.current && actorAccountID.current > 0) { Navigation.navigate(ROUTES.PROFILE.getRoute(actorAccountID.current)); @@ -198,12 +202,4 @@ function AvatarWithDisplayName({ AvatarWithDisplayName.displayName = 'AvatarWithDisplayName'; -export default withOnyx({ - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '-1'}`, - canEvict: false, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, -})(AvatarWithDisplayName); +export default AvatarWithDisplayName; From 54071b44adb6b001609664f038ff10e6d70583c3 Mon Sep 17 00:00:00 2001 From: Github Date: Thu, 30 Jan 2025 16:00:18 +0100 Subject: [PATCH 040/201] Perf: improve performance of filterAndOrderOptions --- src/libs/OptionsListUtils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a8e23888db0d..3b3cd5d15853 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2050,7 +2050,15 @@ function filterAndOrderOptions(options: Options, searchInputValue: string, confi const orderedOptions = combineOrderingOfReportsAndPersonalDetails(filterResult, searchInputValue, config); // on staging server, in specific cases (see issue) BE returns duplicated personalDetails entries - orderedOptions.personalDetails = orderedOptions.personalDetails.filter((detail, index, array) => array.findIndex((i) => i.login === detail.login) === index); + const uniqueLogins = new Set(); + orderedOptions.personalDetails = orderedOptions.personalDetails.filter((detail) => { + const login = detail.login ?? ''; + if (uniqueLogins.has(login)) { + return false; + } + uniqueLogins.add(login); + return true; + }); return { ...filterResult, From bf4bbfcd3425831a52dbb5b062510bdd349b493b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 30 Jan 2025 16:16:08 +0100 Subject: [PATCH 041/201] create isNewDotInvoice --- src/libs/ReportUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 739147afb9ad..2a4bf7adf790 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1043,6 +1043,10 @@ function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): boole return report?.type === CONST.REPORT.TYPE.INVOICE; } +function isNewDotInvoice(report: OnyxEntry): boolean { + return isInvoiceRoom(getReport(report?.chatReportID)); +} + /** * Checks if a report is an Expense report. */ @@ -9144,6 +9148,7 @@ export { isInvoiceRoom, isInvoiceRoomWithID, isInvoiceReport, + isNewDotInvoice, isOpenInvoiceReport, getDefaultNotificationPreferenceForReport, canWriteInReport, From 0d4ff8e48ed6b50b4721daea9e6cec4f343b438d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 30 Jan 2025 16:30:01 +0100 Subject: [PATCH 042/201] update isNewDotInvoice function to accept invoiceRoomID as parameter --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2a4bf7adf790..9e693492b0c3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1043,8 +1043,8 @@ function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): boole return report?.type === CONST.REPORT.TYPE.INVOICE; } -function isNewDotInvoice(report: OnyxEntry): boolean { - return isInvoiceRoom(getReport(report?.chatReportID)); +function isNewDotInvoice(invoiceRoomID: string | undefined): boolean { + return isInvoiceRoom(getReport(invoiceRoomID)); } /** From edb48d87bd429a0e91abe7d0c6c4e3f76c350f71 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 30 Jan 2025 16:30:23 +0100 Subject: [PATCH 043/201] integrate isNewDotInvoice --- src/libs/ReportUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9e693492b0c3..b3aa2dceb052 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4322,7 +4322,9 @@ function getReportName( } if (isInvoiceReport(report)) { - formattedName = report?.reportName ?? getMoneyRequestReportName(report, policy, invoiceReceiverPolicy); + const moneyRequestReportName = getMoneyRequestReportName(report, policy, invoiceReceiverPolicy); + const ODInvoiceName = report?.reportName ?? moneyRequestReportName; + formattedName = isNewDotInvoice(report?.chatReportID) ? moneyRequestReportName : ODInvoiceName; } if (isInvoiceRoom(report)) { @@ -4918,6 +4920,7 @@ function buildOptimisticInvoiceReport( total: number, currency: string, ): OptimisticExpenseReport { + const formattedTotal = convertToDisplayString(total, currency); const invoiceReport = { reportID: generateReportID(), chatReportID, @@ -4927,7 +4930,7 @@ function buildOptimisticInvoiceReport( managerID: receiverAccountID, currency, // We don’t translate reportName because the server response is always in English - reportName: `Invoice ${DateUtils.extractDate(new Date().toString())}`, + reportName: isNewDotInvoice(chatReportID) ? `${receiverName} owes ${formattedTotal}` : `Invoice ${DateUtils.extractDate(new Date().toString())}`, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.OPEN, total, From fb1e21b54084e336492ecb61cabfe53297791477 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 30 Jan 2025 16:35:48 +0100 Subject: [PATCH 044/201] revert reportName of buildOptimisticInvoiceReport --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b3aa2dceb052..516071a7e578 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4184,6 +4184,7 @@ function buildReportNameFromParticipantNames({report, personalDetails}: {report: return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', '); } +// #region - getReportName /** * Get the title for a report. */ @@ -4911,6 +4912,7 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe return result.trim().length ? result : formula; } +// #region - buildOptimisticInvoiceReport /** Builds an optimistic invoice report with a randomly generated reportID */ function buildOptimisticInvoiceReport( chatReportID: string, @@ -4930,7 +4932,7 @@ function buildOptimisticInvoiceReport( managerID: receiverAccountID, currency, // We don’t translate reportName because the server response is always in English - reportName: isNewDotInvoice(chatReportID) ? `${receiverName} owes ${formattedTotal}` : `Invoice ${DateUtils.extractDate(new Date().toString())}`, + reportName: `${receiverName} owes ${formattedTotal}`, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.OPEN, total, From bc93838b0a8e6745ff735c16a1f42286aedab50f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 30 Jan 2025 16:36:32 +0100 Subject: [PATCH 045/201] clear --- src/libs/ReportUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 516071a7e578..9dcdb40eeab7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4184,7 +4184,6 @@ function buildReportNameFromParticipantNames({report, personalDetails}: {report: return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', '); } -// #region - getReportName /** * Get the title for a report. */ @@ -4912,7 +4911,6 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe return result.trim().length ? result : formula; } -// #region - buildOptimisticInvoiceReport /** Builds an optimistic invoice report with a randomly generated reportID */ function buildOptimisticInvoiceReport( chatReportID: string, From 2a3c29e9b7b34d8593d0ffc34a8b324222102c66 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 30 Jan 2025 17:46:17 +0100 Subject: [PATCH 046/201] Refactor shouldFetchReport function to reduce updates --- src/libs/shouldFetchReport.ts | 5 ++--- src/pages/home/ReportScreen.tsx | 7 ++++--- src/pages/home/report/ReportActionsView.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/shouldFetchReport.ts b/src/libs/shouldFetchReport.ts index ae0384bf728d..07d416e98ffc 100644 --- a/src/libs/shouldFetchReport.ts +++ b/src/libs/shouldFetchReport.ts @@ -1,9 +1,8 @@ import type {OnyxEntry} from 'react-native-onyx'; import type Report from '@src/types/onyx/Report'; -import type ReportMetadata from '@src/types/onyx/ReportMetadata'; -export default function shouldFetchReport(report: OnyxEntry, reportMetadata: OnyxEntry) { +export default function shouldFetchReport(report: OnyxEntry, isOptimisticReport?: boolean) { // If the report is optimistic, there's no need to fetch it. The original action should create it. // If there is an error for creating the chat, there's no need to fetch it since it doesn't exist - return !reportMetadata?.isOptimisticReport && !report?.errorFields?.createChat; + return !isOptimisticReport && !report?.errorFields?.createChat; } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 39a670b43284..68a513ddc2ce 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -518,7 +518,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } - if (!shouldFetchReport(report, reportMetadata)) { + if (!shouldFetchReport(report, reportMetadata.isOptimisticReport)) { return; } // When creating an optimistic report that already exists, we need to skip openReport @@ -529,7 +529,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } fetchReport(); - }, [reportIDFromRoute, isLoadingApp, report, reportMetadata, fetchReport]); + }, [reportIDFromRoute, isLoadingApp, report, fetchReport, reportMetadata]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -575,8 +575,9 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } fetchReportIfNeeded(); + console.log('fetchReportIfNeeded'); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isLoadingReportOnyx]); + }, [isLoadingReportOnyx, fetchReportIfNeeded]); useEffect(() => { if (isLoadingReportOnyx || !reportActionIDFromRoute || isLinkedMessagePageReady) { diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index ceb704b30e6a..99327cbef2d8 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -118,7 +118,7 @@ function ReportActionsView({ const reportID = report.reportID; const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); const openReportIfNecessary = () => { - if (!shouldFetchReport(report, reportMetadata)) { + if (!shouldFetchReport(report, reportMetadata?.isOptimisticReport)) { return; } From a5554938801a64582a9871d68cbacac7c73aa9aa Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 30 Jan 2025 17:50:28 +0100 Subject: [PATCH 047/201] Remove log --- src/pages/home/ReportScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 68a513ddc2ce..ea16d28a47ac 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -575,7 +575,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } fetchReportIfNeeded(); - console.log('fetchReportIfNeeded'); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isLoadingReportOnyx, fetchReportIfNeeded]); From 40d29a02b9658c202527a09e25bcdd8920ebe87c Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Thu, 30 Jan 2025 18:29:23 -0500 Subject: [PATCH 048/201] Use minutes instead of months --- src/libs/DateUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 6952324a33d3..d5ab8c871c04 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -819,9 +819,9 @@ function getFormattedReservationRangeDate(date1: Date, date2: Date): string { */ function getFormattedTransportDate(date: Date): string { if (isThisYear(date)) { - return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; + return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:mm a')}`; } - return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; + return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:mm a')}`; } /** From 4886fb1ca562e75050dae1776d31d053b8418596 Mon Sep 17 00:00:00 2001 From: Prakash Baskaran Date: Wed, 29 Jan 2025 14:03:58 +0530 Subject: [PATCH 049/201] Web - Chat - Pasted message not always displays no hyperlink format when paste as plain text --- src/hooks/useHtmlPaste/index.ts | 6 +++--- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts index 1a7e62f3141e..67f4117d0e6b 100644 --- a/src/hooks/useHtmlPaste/index.ts +++ b/src/hooks/useHtmlPaste/index.ts @@ -89,9 +89,9 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, isActive */ const handlePastePlainText = useCallback( (event: ClipboardEvent) => { - const plainText = event.clipboardData?.getData('text/plain'); - if (plainText) { - paste(plainText); + const markdownText = event.clipboardData?.getData('text/plain'); + if (markdownText) { + paste(Parser.htmlToText(Parser.replace(markdownText))); } }, [paste], diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index bf5e1b253f3f..694d8eea5ab5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -119,10 +119,8 @@ function setClipboardMessage(content: string | undefined) { if (!Clipboard.canSetHtml()) { Clipboard.setString(Parser.htmlToMarkdown(content)); } else { - const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; - const isAnchorTag = anchorRegex.test(content); - const plainText = isAnchorTag ? Parser.htmlToMarkdown(content) : Parser.htmlToText(content); - Clipboard.setHtml(content, plainText); + const markdownText = Parser.htmlToMarkdown(content); + Clipboard.setHtml(content, markdownText); } } From 19f0d68f498d056d43c591cd8855e1a1d0992732 Mon Sep 17 00:00:00 2001 From: M00rish Date: Fri, 31 Jan 2025 12:27:09 +0100 Subject: [PATCH 050/201] fix Not found page blinking --- src/pages/home/ReportScreen.tsx | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 39a670b43284..c1b809c932b6 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -386,7 +386,9 @@ function ReportScreen({route, navigation}: ReportScreenProps) { () => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report)), [linkedAction, report], ); - const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); + + const [isClearingDeletedLinkedAction, setIsClearingDeletedLinkedAction] = useState(); + const isLinkedActionInaccessibleWhisper = useMemo( () => !!linkedAction && isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), [currentUserAccountID, linkedAction], @@ -416,11 +418,9 @@ function ReportScreen({route, navigation}: ReportScreenProps) { (!!deleteTransactionNavigateBackUrl && getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) || (!reportMetadata.isOptimisticReport && isLoading); - const isLinkedActionBecomesDeleted = prevIsLinkedActionDeleted !== undefined && !prevIsLinkedActionDeleted && isLinkedActionDeleted; - // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundLinkedAction = - (!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && !isLinkedActionBecomesDeleted) || + (!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && isClearingDeletedLinkedAction === false) || (shouldShowSkeleton && !reportMetadata.isLoadingInitialReportActions && !!reportActionIDFromRoute && @@ -735,13 +735,23 @@ function ReportScreen({route, navigation}: ReportScreenProps) { }, [fetchReport]); useEffect(() => { - // If the linked action is previously available but now deleted, - // remove the reportActionID from the params to not link to the deleted action. - if (!isLinkedActionBecomesDeleted) { + + if (!isLinkedActionDeleted || isClearingDeletedLinkedAction) + { return; } + Navigation.setParams({reportActionID: ''}); - }, [isLinkedActionBecomesDeleted]); + setIsClearingDeletedLinkedAction(true); + }, [isLinkedActionDeleted, isClearingDeletedLinkedAction]); + + + useEffect(() => { + if (!isClearingDeletedLinkedAction || reportActionIDFromRoute) { + return; + } + setIsClearingDeletedLinkedAction(false); + }, [isClearingDeletedLinkedAction, reportActionIDFromRoute]); // If user redirects to an inaccessible whisper via a deeplink, on a report they have access to, // then we set reportActionID as empty string, so we display them the report and not the "Not found page". From 0cf5ebbad04eb8c78b637bc29bc6bf599b1e5a35 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 31 Jan 2025 13:03:51 +0100 Subject: [PATCH 051/201] working highlighting --- src/libs/runOnLiveMarkdownRuntime/index.native.tsx | 8 ++++++++ src/libs/runOnLiveMarkdownRuntime/index.tsx | 6 ++++++ 2 files changed, 14 insertions(+) create mode 100644 src/libs/runOnLiveMarkdownRuntime/index.native.tsx create mode 100644 src/libs/runOnLiveMarkdownRuntime/index.tsx diff --git a/src/libs/runOnLiveMarkdownRuntime/index.native.tsx b/src/libs/runOnLiveMarkdownRuntime/index.native.tsx new file mode 100644 index 000000000000..8927fe0a936c --- /dev/null +++ b/src/libs/runOnLiveMarkdownRuntime/index.native.tsx @@ -0,0 +1,8 @@ +import {getWorkletRuntime} from '@expensify/react-native-live-markdown'; +import {runOnRuntime} from 'react-native-reanimated'; + +function runOnLiveMarkdownRuntime(worklet: (...args: Args) => ReturnValue) { + return runOnRuntime(getWorkletRuntime(), worklet); +} + +export default runOnLiveMarkdownRuntime; diff --git a/src/libs/runOnLiveMarkdownRuntime/index.tsx b/src/libs/runOnLiveMarkdownRuntime/index.tsx new file mode 100644 index 000000000000..c8e072693323 --- /dev/null +++ b/src/libs/runOnLiveMarkdownRuntime/index.tsx @@ -0,0 +1,6 @@ +// Reanimated does not support runOnRuntime() on web +function runOnLiveMarkdownRuntime(worklet: (...args: Args) => ReturnValue) { + return worklet; +} + +export default runOnLiveMarkdownRuntime; From 85d543554cc3b1a4320de755eb2a95518334ac19 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 31 Jan 2025 13:13:16 +0100 Subject: [PATCH 052/201] add syntax highlighting --- .../Search/SearchAutocompleteInput.tsx | 89 +++++++++++++++++-- .../SearchInputSelectionWrapper/index.tsx | 2 +- .../Search/SearchPageHeaderInput.tsx | 1 + .../Search/SearchRouter/SearchRouter.tsx | 1 + src/libs/SearchAutocompleteUtils.ts | 52 ++++++++--- 5 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 215599952c50..3e260a9bda96 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -1,20 +1,27 @@ +import isEqual from 'lodash/isEqual'; import type {ForwardedRef, ReactNode, RefObject} from 'react'; -import React, {forwardRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import {useSharedValue} from 'react-native-reanimated'; import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import {parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; +import runOnLiveMarkdownRuntime from '@libs/runOnLiveMarkdownRuntime'; +import {getAutocompleteCategories, getAutocompleteTags, parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; import handleKeyPress from '@libs/SearchInputOnKeyPress'; import shouldDelayFocus from '@libs/shouldDelayFocus'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SubstitutionMap} from './SearchRouter/getQueryWithSubstitutions'; type SearchAutocompleteInputProps = { /** Value of TextInput */ @@ -59,6 +66,9 @@ type SearchAutocompleteInputProps = { /** Whether the search reports API call is running */ isSearchingForReports?: boolean; + /** Map of autocomplete suggestions. Required for highlighting to work properly */ + substitutionMap: SubstitutionMap; + /** input style */ inputStyle?: StyleProp; } & Pick; @@ -83,6 +93,7 @@ function SearchAutocompleteInput( isSearchingForReports, selection, inputStyle, + substitutionMap, }: SearchAutocompleteInputProps, ref: ForwardedRef, ) { @@ -90,9 +101,75 @@ function SearchAutocompleteInput( const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); const {isOffline} = useNetwork(); + const {activeWorkspaceID} = useActiveWorkspace(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const personalDetailsSharedValue = useSharedValue({login: currentUserPersonalDetails.login, userDisplayName: currentUserPersonalDetails.displayName}); + const lastMap = useRef({}); + const [map, setMap] = useState({}); + + const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); + const currencyAutocompleteList = Object.keys(currencyList ?? {}); + const currencySharedValue = useSharedValue(currencyAutocompleteList); + + const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); + const categoryAutocompleteList = useMemo(() => { + return getAutocompleteCategories(allPolicyCategories, activeWorkspaceID); + }, [activeWorkspaceID, allPolicyCategories]); + const categorySharedValue = useSharedValue(categoryAutocompleteList); + + const [allPoliciesTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); + const tagAutocompleteList = useMemo(() => { + return getAutocompleteTags(allPoliciesTags, activeWorkspaceID); + }, [activeWorkspaceID, allPoliciesTags]); + const tagSharedValue = useSharedValue(tagAutocompleteList); + + useEffect(() => { + if (lastMap.current && !isEqual(lastMap.current, substitutionMap)) { + lastMap.current = substitutionMap; + } + setMap(lastMap.current ?? {}); + }, [substitutionMap, lastMap]); + const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + personalDetailsSharedValue.set({login: currentUserPersonalDetails.login, userDisplayName: currentUserPersonalDetails.displayName}); + })(); + }, [currentUserPersonalDetails, personalDetailsSharedValue]); + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + currencySharedValue.set(currencyAutocompleteList); + })(); + }, [currencyAutocompleteList, currencySharedValue]); + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + categorySharedValue.set(categoryAutocompleteList); + })(); + }, [categorySharedValue, categoryAutocompleteList]); + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + tagSharedValue.set(tagAutocompleteList); + }); + }, [tagSharedValue, tagAutocompleteList]); + + const parser = useCallback( + (input: string) => { + 'worklet'; + + return parseForLiveMarkdown(input, personalDetailsSharedValue, map, currencySharedValue, categorySharedValue, tagSharedValue); + }, + [personalDetailsSharedValue, map, currencySharedValue, categorySharedValue, tagSharedValue], + ); + const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; return ( @@ -121,7 +198,7 @@ function SearchAutocompleteInput( maxLength={CONST.SEARCH_QUERY_LIMIT} onSubmitEditing={onSubmit} shouldUseDisabledStyles={false} - textInputContainerStyles={[styles.borderNone, styles.pb0]} + textInputContainerStyles={[styles.borderNone, styles.pb0, styles.pr3]} inputStyle={[inputWidth, inputStyle]} onFocus={() => { setIsFocused(true); @@ -138,11 +215,7 @@ function SearchAutocompleteInput( onKeyPress={handleKeyPress(onSubmit)} isMarkdownEnabled multiline={false} - parser={(input: string) => { - 'worklet'; - - return parseForLiveMarkdown(input, currentUserPersonalDetails.login ?? '', currentUserPersonalDetails.displayName ?? ''); - }} + parser={parser} selection={selection} /> diff --git a/src/components/Search/SearchInputSelectionWrapper/index.tsx b/src/components/Search/SearchInputSelectionWrapper/index.tsx index a806cea9afd4..1a0a18ffaa47 100644 --- a/src/components/Search/SearchInputSelectionWrapper/index.tsx +++ b/src/components/Search/SearchInputSelectionWrapper/index.tsx @@ -10,7 +10,7 @@ function SearchInputSelectionWrapper({selection, ...props}: SearchAutocompleteIn return ( , + map: SubstitutionMap, + currencyList: SharedValue, + categoryList: SharedValue, + tagList: SharedValue, +) { 'worklet'; const parsedAutocomplete = parse(input) as SearchAutocompleteResult; const ranges = parsedAutocomplete.ranges; - return ranges.map((range) => { - let type = 'mention-user'; - - if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (range.value === userLogin || range.value === userDisplayName)) { - type = 'mention-here'; - } - - return {...range, type}; - }) as MarkdownRange[]; + const typeList = Object.values(CONST.SEARCH.DATA_TYPES) as string[]; + const expenseTypeList = Object.values(CONST.SEARCH.TRANSACTION_TYPE) as string[]; + const statusList = Object.values({...CONST.SEARCH.STATUS.TRIP, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP}) as string[]; + const subMap = map; + return ranges + .filter( + (range) => + !( + range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || + range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || + range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN || + range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE || + range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID + ) || subMap[`${range.key}:${range.value}`] !== undefined, + ) + .filter((range) => range.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.CURRENCY || currencyList.get().includes(range.value)) + .filter((range) => range.key !== CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE || typeList.includes(range.value)) + .filter((range) => range.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.EXPENSE_TYPE || expenseTypeList.includes(range.value)) + .filter((range) => range.key !== CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS || statusList.includes(range.value)) + .filter((range) => range.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.CATEGORY || categoryList.get().includes(range.value)) + .filter((range) => range.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG || tagList.get().includes(range.value)) + .map((range) => { + let type = 'mention-user'; + if ( + (range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && + (range.value === details.get().login || range.value === details.get().userDisplayName) + ) { + type = 'mention-here'; + } + + return {...range, type}; + }) as MarkdownRange[]; } export { From 944fbdd2344a2663746588dc29ed65d064922b40 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 31 Jan 2025 13:35:45 +0100 Subject: [PATCH 053/201] add supporting text --- .../Search/SearchRouter/buildSubstitutionsMap.ts | 2 +- .../Search/SearchRouter/getQueryWithSubstitutions.ts | 2 +- .../Search/SearchRouter/getUpdatedSubstitutionsMap.ts | 2 +- src/components/Search/types.ts | 4 +++- src/libs/SearchAutocompleteUtils.ts | 3 +++ src/libs/SearchParser/autocompleteParser.js | 9 +++++---- src/libs/SearchParser/autocompleteParser.peggy | 9 +++++---- 7 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/components/Search/SearchRouter/buildSubstitutionsMap.ts b/src/components/Search/SearchRouter/buildSubstitutionsMap.ts index 892aa050ef9d..0a3c49819b38 100644 --- a/src/components/Search/SearchRouter/buildSubstitutionsMap.ts +++ b/src/components/Search/SearchRouter/buildSubstitutionsMap.ts @@ -33,7 +33,7 @@ function buildSubstitutionsMap( ): SubstitutionMap { const parsedQuery = parse(query) as {ranges: SearchAutocompleteQueryRange[]}; - const searchAutocompleteQueryRanges = parsedQuery.ranges; + const searchAutocompleteQueryRanges = parsedQuery.ranges.filter((range) => range.key !== 'syntax'); if (searchAutocompleteQueryRanges.length === 0) { return {}; diff --git a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts index 84895efb33a5..b976371135a6 100644 --- a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts +++ b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts @@ -21,7 +21,7 @@ const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${ function getQueryWithSubstitutions(changedQuery: string, substitutions: SubstitutionMap) { const parsed = parse(changedQuery) as {ranges: SearchAutocompleteQueryRange[]}; - const searchAutocompleteQueryRanges = parsed.ranges; + const searchAutocompleteQueryRanges = parsed.ranges.filter((range) => range.key !== 'syntax'); if (searchAutocompleteQueryRanges.length === 0) { return changedQuery; diff --git a/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts b/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts index ee7bf3850259..a681ae6030f0 100644 --- a/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts +++ b/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts @@ -18,7 +18,7 @@ const getSubstitutionsKey = (filterKey: SearchFilterKey, value: string) => `${fi function getUpdatedSubstitutionsMap(query: string, substitutions: SubstitutionMap): SubstitutionMap { const parsedQuery = parser.parse(query) as {ranges: SearchAutocompleteQueryRange[]}; - const searchAutocompleteQueryRanges = parsedQuery.ranges; + const searchAutocompleteQueryRanges = parsedQuery.ranges.filter((range) => range.key !== 'syntax'); if (searchAutocompleteQueryRanges.length === 0) { return {}; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 0a402358d73e..b5c58c798b9f 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -95,7 +95,8 @@ type SearchFilterKey = | ValueOf | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS - | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID; + | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID + | 'syntax'; type UserFriendlyKey = ValueOf; @@ -128,6 +129,7 @@ type SearchAutocompleteResult = { ranges: SearchAutocompleteQueryRange[]; }; +// TODO FIX types type SearchAutocompleteQueryRange = { key: SearchFilterKey; length: number; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index a591e08c2314..d9abec7abf3e 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -182,6 +182,9 @@ function parseForLiveMarkdown( ) { type = 'mention-here'; } + if (range.key === 'syntax') { + type = 'syntax'; + } return {...range, type}; }) as MarkdownRange[]; diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 5a787c5d8048..16e135deb222 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -282,20 +282,21 @@ function peg$parse(input, options) { start: location().end.offset, length: 0, }; - return; + return {key:"syntax",value:key, start:location().start.offset, length:location().end.offset-location().start.offset}; } autocomplete = { key, ...value[value.length - 1], }; - - return value + const result = value .filter((filter) => filter.length > 0) .map((filter) => ({ key, ...filter, - })); + })) + + return [{key:"syntax",value:key, start:location().start.offset, length:result[0].start - location().start.offset}, ...result]; }; var peg$f3 = function() { autocomplete = null; }; var peg$f4 = function(parts, empty) { diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 928d1751f8ce..93b1b7e52fa7 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -31,20 +31,21 @@ defaultFilter start: location().end.offset, length: 0, }; - return; + return {key:"syntax",value:key, start:location().start.offset, length:location().end.offset-location().start.offset}; } autocomplete = { key, ...value[value.length - 1], }; - - return value + const result = value .filter((filter) => filter.length > 0) .map((filter) => ({ key, ...filter, - })); + })) + + return [{key:"syntax",value:key, start:location().start.offset, length:result[0].start - location().start.offset}, ...result]; } freeTextFilter = _ (identifier/ ",") _ { autocomplete = null; } From 7eb5861937a61f2f1ff5408dcc7c171bccd987b6 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Fri, 31 Jan 2025 13:47:35 +0100 Subject: [PATCH 054/201] grammar formatting --- src/libs/SearchParser/autocompleteParser.js | 23 +++++++++++++---- .../SearchParser/autocompleteParser.peggy | 25 ++++++++++++++----- src/libs/SearchParser/searchParser.js | 18 ++++++------- src/libs/SearchParser/searchParser.peggy | 18 +++++++------ 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index 16e135deb222..092a0022889f 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -278,11 +278,16 @@ function peg$parse(input, options) { if (!value) { autocomplete = { key, - value: '', + value: "", start: location().end.offset, length: 0, }; - return {key:"syntax",value:key, start:location().start.offset, length:location().end.offset-location().start.offset}; + return { + key: "syntax", + value: key, + start: location().start.offset, + length: location().end.offset - location().start.offset, + }; } autocomplete = { @@ -294,9 +299,17 @@ function peg$parse(input, options) { .map((filter) => ({ key, ...filter, - })) - - return [{key:"syntax",value:key, start:location().start.offset, length:result[0].start - location().start.offset}, ...result]; + })); + + return [ + { + key: "syntax", + value: key, + start: location().start.offset, + length: result[0].start - location().start.offset, + }, + ...result, + ]; }; var peg$f3 = function() { autocomplete = null; }; var peg$f4 = function(parts, empty) { diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 93b1b7e52fa7..0b8e15f3364a 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -27,11 +27,16 @@ defaultFilter if (!value) { autocomplete = { key, - value: '', + value: "", start: location().end.offset, length: 0, }; - return {key:"syntax",value:key, start:location().start.offset, length:location().end.offset-location().start.offset}; + return { + key: "syntax", + value: key, + start: location().start.offset, + length: location().end.offset - location().start.offset, + }; } autocomplete = { @@ -43,12 +48,20 @@ defaultFilter .map((filter) => ({ key, ...filter, - })) - - return [{key:"syntax",value:key, start:location().start.offset, length:result[0].start - location().start.offset}, ...result]; + })); + + return [ + { + key: "syntax", + value: key, + start: location().start.offset, + length: result[0].start - location().start.offset, + }, + ...result, + ]; } -freeTextFilter = _ (identifier/ ",") _ { autocomplete = null; } +freeTextFilter = _ (identifier / ",") _ { autocomplete = null; } autocompleteKey "key" = @( diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 804a5928a909..aee1033bef31 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -298,7 +298,9 @@ function peg$parse(input, options) { const keywordFilter = buildFilter( "eq", "keyword", - keywords.map((filter) => filter.right).flat() + keywords + .map((filter) => filter.right.replace(/^(['"])(.*)\1$/, "$2")) + .flat() ); if (keywordFilter.right.length > 0) { nonKeywords.push(keywordFilter); @@ -310,20 +312,18 @@ function peg$parse(input, options) { var peg$f2 = function(key, op, value) { updateDefaultValues(key, value); }; - var peg$f3 = function(value) { //handle no-breaking space - let word + var peg$f3 = function(value) { + //handle no-breaking space + let word; if (Array.isArray(value)) { - word = value.join("") - // return buildFilter("eq", "keyword", value.join("")); - }else{ - word = value + word = value.join(""); + } else { + word = value; } if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { return buildFilter("eq", "keyword", word.slice(1, -1)); } return buildFilter("eq", "keyword", word); - - // return buildFilter("eq", "keyword", value); }; var peg$f4 = function(field, op, values) { return buildFilter(op, field, values); diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 28f5c3d8a96f..3bbe57722b88 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -66,7 +66,9 @@ filterList const keywordFilter = buildFilter( "eq", "keyword", - keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat() + keywords + .map((filter) => filter.right.replace(/^(['"])(.*)\1$/, "$2")) + .flat() ); if (keywordFilter.right.length > 0) { nonKeywords.push(keywordFilter); @@ -84,12 +86,13 @@ defaultFilter } freeTextFilter - = _ value:(quotedString / [^ \t\r\n\xA0]+) _ { //handle no-breaking space - let word + = _ value:(quotedString / [^ \t\r\n\xA0]+) _ { + //handle no-breaking space + let word; if (Array.isArray(value)) { - word = value.join("") - }else{ - word = value + word = value.join(""); + } else { + word = value; } if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { return buildFilter("eq", "keyword", word.slice(1, -1)); @@ -126,8 +129,7 @@ key "key" / posted ) -defaultKey "default key" - = @(type / status / sortBy / sortOrder / policyID) +defaultKey "default key" = @(type / status / sortBy / sortOrder / policyID) identifier = (","+)? parts:(quotedString / alphanumeric)|1.., ","+| empty:(","+)? { From 23f4d410a3c39b8fc884a70032a17795790b00ef Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 1 Feb 2025 15:37:51 +0700 Subject: [PATCH 055/201] fix backpress event --- src/hooks/useSearchBackPress/index.android.ts | 28 +++++++++++++++++++ src/hooks/useSearchBackPress/index.ts | 6 ++++ src/hooks/useSearchBackPress/types.ts | 9 ++++++ src/pages/ReportParticipantsPage.tsx | 13 +++++++++ src/pages/RoomMembersPage.tsx | 9 ++++++ src/pages/workspace/WorkspaceMembersPage.tsx | 6 ++++ .../categories/WorkspaceCategoriesPage.tsx | 6 ++++ .../distanceRates/PolicyDistanceRatesPage.tsx | 6 ++++ .../perDiem/WorkspacePerDiemPage.tsx | 8 ++++++ .../ReportFieldsListValuesPage.tsx | 8 ++++++ .../workspace/tags/WorkspaceTagsPage.tsx | 8 ++++++ .../workspace/tags/WorkspaceViewTagsPage.tsx | 8 ++++++ .../workspace/taxes/WorkspaceTaxesPage.tsx | 8 ++++++ 13 files changed, 123 insertions(+) create mode 100644 src/hooks/useSearchBackPress/index.android.ts create mode 100644 src/hooks/useSearchBackPress/index.ts create mode 100644 src/hooks/useSearchBackPress/types.ts diff --git a/src/hooks/useSearchBackPress/index.android.ts b/src/hooks/useSearchBackPress/index.android.ts new file mode 100644 index 000000000000..3063a48116f2 --- /dev/null +++ b/src/hooks/useSearchBackPress/index.android.ts @@ -0,0 +1,28 @@ +import {useFocusEffect} from '@react-navigation/native'; +import {useCallback} from 'react'; +import {BackHandler} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type UseSearchBackPress from './types'; + +const useSearchBackPress: UseSearchBackPress = ({onClearSelection, onNavigationCallBack}) => { + const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE); + useFocusEffect( + useCallback(() => { + const onBackPress = () => { + if (selectionMode?.isEnabled) { + onClearSelection(); + turnOffMobileSelectionMode(); + return true; + } + onNavigationCallBack(); + return false; + }; + BackHandler.addEventListener('hardwareBackPress', onBackPress); + return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress); + }, [selectionMode, onClearSelection, onNavigationCallBack]), + ); +}; + +export default useSearchBackPress; diff --git a/src/hooks/useSearchBackPress/index.ts b/src/hooks/useSearchBackPress/index.ts new file mode 100644 index 000000000000..7cc904a7f31b --- /dev/null +++ b/src/hooks/useSearchBackPress/index.ts @@ -0,0 +1,6 @@ +import type UseSearchBackPress from './types'; + +// the back press event is only supported on Android native +const useSearchBackPress: UseSearchBackPress = () => {}; + +export default useSearchBackPress; diff --git a/src/hooks/useSearchBackPress/types.ts b/src/hooks/useSearchBackPress/types.ts new file mode 100644 index 000000000000..88a2aa0d5964 --- /dev/null +++ b/src/hooks/useSearchBackPress/types.ts @@ -0,0 +1,9 @@ +type UseSearchBackPressParams = { + onClearSelection: () => void; + onNavigationCallBack: () => void; + backTo?: string; +}; + +type UseSearchBackPress = (params: UseSearchBackPressParams) => void; + +export default UseSearchBackPress; diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 1bb36bf89080..87e650d72873 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -21,6 +21,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -107,6 +108,18 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { } }, [isFocused, setSearchValue, shouldShowTextInput, userSearchPhrase]); + useSearchBackPress({ + onClearSelection: () => setSelectedMembers([]), + onNavigationCallBack: () => { + if (!report) { + return; + } + + setSearchValue(''); + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo)); + }, + }); + const getParticipants = () => { let result: MemberOption[] = []; diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index a14ada3d3f00..afdd8705a1ef 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -20,6 +20,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as UserSearchPhraseActions from '@libs/actions/RoomMembersUserSearchPhrase'; @@ -209,6 +210,14 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { } }, [isFocusedScreen, setSearchValue, shouldShowTextInput, userSearchPhrase]); + useSearchBackPress({ + onClearSelection: () => setSelectedMembers([]), + onNavigationCallBack: () => { + setSearchValue(''); + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo)); + }, + }); + const data = useMemo((): ListItem[] => { let result: ListItem[] = []; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 8c3e4152af38..f2a3b9581603 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -27,6 +27,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -454,6 +455,11 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson setSelectedEmployees([]); }, [setSelectedEmployees, selectionMode?.isEnabled]); + useSearchBackPress({ + onClearSelection: () => setSelectedEmployees([]), + onNavigationCallBack: () => Navigation.goBack(), + }); + const getCustomListHeader = () => { return ( setSelectedCategories({}), + onNavigationCallBack: () => Navigation.goBack(backTo), + }); + const updateWorkspaceRequiresCategory = useCallback( (value: boolean, categoryName: string) => { setWorkspaceCategoryEnabled(policyId, {[categoryName]: {name: categoryName, enabled: value}}); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 3c7a969f4057..9936b69ba5d6 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -19,6 +19,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -108,6 +109,11 @@ function PolicyDistanceRatesPage({ setSelectedDistanceRates([]); }, [isFocused]); + useSearchBackPress({ + onClearSelection: () => setSelectedDistanceRates([]), + onNavigationCallBack: () => Navigation.goBack(), + }); + const updateDistanceRateEnabled = useCallback( (value: boolean, rateID: string) => { if (!customUnit) { diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 1f51af583d8a..5ff8dd9caa29 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -24,6 +24,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -279,6 +280,13 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { setSelectedPerDiem([]); }, [setSelectedPerDiem, selectionMode?.isEnabled]); + useSearchBackPress({ + onClearSelection: () => { + setSelectedPerDiem([]); + }, + onNavigationCallBack: () => Navigation.goBack(backTo), + }); + const hasVisibleSubRates = subRatesList.some((subRate) => subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); const getHeaderText = () => ( diff --git a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx index fa5a911b51c3..2c23f1877523 100644 --- a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx @@ -20,6 +20,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import { @@ -107,6 +108,13 @@ function ReportFieldsListValuesPage({ [policyID, reportFieldID], ); + useSearchBackPress({ + onClearSelection: () => { + setSelectedValues({}); + }, + onNavigationCallBack: () => Navigation.goBack(), + }); + const listValuesSections = useMemo(() => { const data = listValues .map((value, index) => ({ diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 5602acec4767..298effde4158 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -28,6 +28,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -101,6 +102,13 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { setSelectedTags({}); }, [isFocused]); + useSearchBackPress({ + onClearSelection: () => { + setSelectedTags({}); + }, + onNavigationCallBack: () => Navigation.goBack(backTo), + }); + const getPendingAction = (policyTagList: PolicyTagList): PendingAction | undefined => { if (!policyTagList) { return undefined; diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index 6f54a36d92b8..f378e9c29e65 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -19,6 +19,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -88,6 +89,13 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { }; }, [isFocused]); + useSearchBackPress({ + onClearSelection: () => { + setSelectedTags({}); + }, + onNavigationCallBack: () => Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID) : undefined), + }); + const updateWorkspaceTagEnabled = useCallback( (value: boolean, tagName: string) => { setWorkspaceTagEnabled(policyID, {[tagName]: {name: tagName, enabled: value}}, route.params.orderWeight); diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 8e8696afb1d2..a72d2206fae9 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -22,6 +22,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isConnectionInProgress} from '@libs/actions/connections'; @@ -93,6 +94,13 @@ function WorkspaceTaxesPage({ setSelectedTaxesIDs([]); }, [isFocused]); + useSearchBackPress({ + onClearSelection: () => { + setSelectedTaxesIDs([]); + }, + onNavigationCallBack: () => Navigation.goBack(), + }); + const textForDefault = useCallback( (taxID: string, taxRate: TaxRate): string => { let suffix; From f327ed0fe9c689b537aa92025cf02c8bab59e3ff Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 1 Feb 2025 16:46:55 +0700 Subject: [PATCH 056/201] address linting --- src/pages/ReportParticipantsPage.tsx | 67 ++++++------ src/pages/RoomMembersPage.tsx | 60 +++++------ src/pages/workspace/WorkspaceMembersPage.tsx | 102 ++++++++++-------- .../categories/WorkspaceCategoriesPage.tsx | 2 +- 4 files changed, 122 insertions(+), 109 deletions(-) diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 87e650d72873..60aa0bd9d555 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -11,7 +11,7 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; +import {FallbackAvatar, MakeAdmin, Plus, RemoveMembers, User} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; @@ -25,14 +25,26 @@ import useSearchBackPress from '@hooks/useSearchBackPress'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as Report from '@libs/actions/Report'; -import * as UserSearchPhraseActions from '@libs/actions/RoomMembersUserSearchPhrase'; +import {removeFromGroupChat, updateGroupChatMemberRoles} from '@libs/actions/Report'; +import {clearUserSearchPhrase} from '@libs/actions/RoomMembersUserSearchPhrase'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {isSearchStringMatchUserDetails} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; +import { + getParticipantsList, + getReportName, + isArchivedNonExpenseReport, + isChatRoom, + isChatThread, + isGroupChatAdmin, + isGroupChat as isGroupChatUtils, + isMoneyRequestReport, + isPolicyExpenseChat, + isSelfDM, + isTaskReport, +} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -64,8 +76,8 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const [session] = useOnyx(ONYXKEYS.SESSION); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const currentUserAccountID = Number(session?.accountID); - const isCurrentUserAdmin = ReportUtils.isGroupChatAdmin(report, currentUserAccountID); - const isGroupChat = useMemo(() => ReportUtils.isGroupChat(report), [report]); + const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); + const isGroupChat = useMemo(() => isGroupChatUtils(report), [report]); const isFocused = useIsFocused(); const {isOffline} = useNetwork(); const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? selectionMode?.isEnabled : true); @@ -78,7 +90,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { setSelectedMembers([]); }, [isFocused]); - const chatParticipants = ReportUtils.getParticipantsList(report, personalDetails); + const chatParticipants = getParticipantsList(report, personalDetails); const pendingChatMembers = reportMetadata?.pendingChatMembers; const reportParticipants = report?.participants; @@ -103,7 +115,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { if (shouldShowTextInput) { setSearchValue(userSearchPhrase ?? ''); } else { - UserSearchPhraseActions.clearUserSearchPhrase(); + clearUserSearchPhrase(); setSearchValue(''); } }, [isFocused, setSearchValue, shouldShowTextInput, userSearchPhrase]); @@ -128,7 +140,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const details = personalDetails?.[accountID]; // If search value is provided, filter out members that don't match the search value - if (!details || (searchValue.trim() && !OptionsListUtils.isSearchStringMatchUserDetails(details, searchValue))) { + if (!details || (searchValue.trim() && !isSearchStringMatchUserDetails(details, searchValue))) { return; } @@ -148,13 +160,13 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { isSelected, isDisabledCheckbox: accountID === currentUserAccountID, isDisabled: pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || details?.isOptimisticPersonalDetail, - text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + text: formatPhoneNumber(getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, pendingAction, icons: [ { - source: details?.avatar ?? Expensicons.FallbackAvatar, + source: details?.avatar ?? FallbackAvatar, name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, @@ -213,19 +225,19 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const removeUsers = () => { // Remove the admin from the list const accountIDsToRemove = selectedMembers.filter((id) => id !== currentUserAccountID); - Report.removeFromGroupChat(report.reportID, accountIDsToRemove); + removeFromGroupChat(report.reportID, accountIDsToRemove); setSearchValue(''); setSelectedMembers([]); setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { - UserSearchPhraseActions.clearUserSearchPhrase(); + clearUserSearchPhrase(); }); }; const changeUserRole = useCallback( (role: ValueOf) => { const accountIDsToUpdate = selectedMembers.filter((id) => report.participants?.[id].role !== role); - Report.updateGroupChatMemberRoles(report.reportID, accountIDsToUpdate, role); + updateGroupChatMemberRoles(report.reportID, accountIDsToUpdate, role); setSelectedMembers([]); }, [report, selectedMembers], @@ -276,7 +288,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { { text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, - icon: Expensicons.RemoveMembers, + icon: RemoveMembers, onSelected: () => setRemoveMembersConfirmModalVisible(true), }, ]; @@ -287,7 +299,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { options.push({ text: translate('workspace.people.makeMember'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, - icon: Expensicons.User, + icon: User, onSelected: () => changeUserRole(CONST.REPORT.ROLE.MEMBER), }); } @@ -298,7 +310,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { options.push({ text: translate('workspace.people.makeAdmin'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, - icon: Expensicons.MakeAdmin, + icon: MakeAdmin, onSelected: () => changeUserRole(CONST.REPORT.ROLE.ADMIN), }); } @@ -330,7 +342,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { success onPress={inviteUser} text={translate('workspace.invite.member')} - icon={Expensicons.Plus} + icon={Plus} innerStyles={[shouldUseNarrowLayout && styles.alignItemsCenter]} style={[shouldUseNarrowLayout && styles.flexGrow1]} /> @@ -351,14 +363,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { [report, isCurrentUserAdmin, isGroupChat, backTo], ); const headerTitle = useMemo(() => { - if ( - ReportUtils.isChatRoom(report) || - ReportUtils.isPolicyExpenseChat(report) || - ReportUtils.isChatThread(report) || - ReportUtils.isTaskReport(report) || - ReportUtils.isMoneyRequestReport(report) || - isGroupChat - ) { + if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report) || isGroupChat) { return translate('common.members'); } return translate('common.details'); @@ -378,7 +383,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { style={[styles.defaultModalContainer]} testID={ReportParticipantsPage.displayName} > - + { @@ -394,7 +399,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { } }} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS} - subtitle={StringUtils.lineBreaksToSpaces(ReportUtils.getReportName(report))} + subtitle={StringUtils.lineBreaksToSpaces(getReportName(report))} /> {headerButtons} setRemoveMembersConfirmModalVisible(false)} prompt={translate('workspace.people.removeMembersPrompt', { count: selectedMembers.length, - memberName: formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? ''), + memberName: formatPhoneNumber(getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? ''), })} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index afdd8705a1ef..14f0c8b99da6 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -8,7 +8,7 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, RoomMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; +import {FallbackAvatar, Plus, RemoveMembers} from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import TableListItem from '@components/SelectionList/TableListItem'; @@ -23,18 +23,18 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as UserSearchPhraseActions from '@libs/actions/RoomMembersUserSearchPhrase'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {clearUserSearchPhrase, updateUserSearchPhrase} from '@libs/actions/RoomMembersUserSearchPhrase'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp, PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {RoomMembersNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {isPersonalDetailsReady, isSearchStringMatchUserDetails} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; +import {isPolicyEmployee as isPolicyEmployeeUtils, isUserPolicyAdmin} from '@libs/PolicyUtils'; +import {getParticipantsList, getReportName, isChatThread, isDefaultRoom, isPolicyExpenseChat as isPolicyExpenseChatUtils, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; -import * as Report from '@userActions/Report'; +import {clearAddRoomMemberError, openRoomMembersPage, removeFromRoom} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -59,7 +59,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { const [didLoadRoomMembers, setDidLoadRoomMembers] = useState(false); const personalDetails = usePersonalDetails(); const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(report), [report]); + const isPolicyExpenseChat = useMemo(() => isPolicyExpenseChatUtils(report), [report]); const backTo = route.params.backTo; const isFocusedScreen = useIsFocused(); @@ -85,12 +85,12 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { if (!report) { return; } - Report.openRoomMembersPage(report.reportID); + openRoomMembersPage(report.reportID); setDidLoadRoomMembers(true); }, [report]); useEffect(() => { - UserSearchPhraseActions.clearUserSearchPhrase(); + clearUserSearchPhrase(); getRoomMembers(); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -112,13 +112,13 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { */ const removeUsers = () => { if (report) { - Report.removeFromRoom(report.reportID, selectedMembers); + removeFromRoom(report.reportID, selectedMembers); } setSearchValue(''); setSelectedMembers([]); setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { - UserSearchPhraseActions.clearUserSearchPhrase(); + clearUserSearchPhrase(); }); }; @@ -171,7 +171,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { } }; - const participants = useMemo(() => ReportUtils.getParticipantsList(report, personalDetails, true), [report, personalDetails]); + const participants = useMemo(() => getParticipantsList(report, personalDetails, true), [report, personalDetails]); /** Include the search bar when there are 8 or more active members in the selection list */ const shouldShowTextInput = useMemo(() => { @@ -195,7 +195,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { }, [isFocusedScreen, shouldShowTextInput, userSearchPhrase]); useEffect(() => { - UserSearchPhraseActions.updateUserSearchPhrase(searchValue); + updateUserSearchPhrase(searchValue); }, [searchValue]); useEffect(() => { @@ -205,7 +205,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { if (shouldShowTextInput) { setSearchValue(userSearchPhrase ?? ''); } else { - UserSearchPhraseActions.clearUserSearchPhrase(); + clearUserSearchPhrase(); setSearchValue(''); } }, [isFocusedScreen, setSearchValue, shouldShowTextInput, userSearchPhrase]); @@ -225,11 +225,11 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { const details = personalDetails?.[accountID]; // If search value is provided, filter out members that don't match the search value - if (!details || (searchValue.trim() && !OptionsListUtils.isSearchStringMatchUserDetails(details, searchValue))) { + if (!details || (searchValue.trim() && !isSearchStringMatchUserDetails(details, searchValue))) { return; } const pendingChatMember = reportMetadata?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); - const isAdmin = PolicyUtils.isUserPolicyAdmin(policy, details.login); + const isAdmin = isUserPolicyAdmin(policy, details.login); const isDisabled = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || details.isOptimisticPersonalDetail; const isDisabledCheckbox = (isPolicyExpenseChat && isAdmin) || @@ -243,11 +243,11 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { isSelected: selectedMembers.includes(accountID), isDisabled, isDisabledCheckbox, - text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + text: formatPhoneNumber(getDisplayNameOrDefault(details)), alternateText: details?.login ? formatPhoneNumber(details.login) : '', icons: [ { - source: details.avatar ?? Expensicons.FallbackAvatar, + source: details.avatar ?? FallbackAvatar, name: details.login ?? '', type: CONST.ICON_TYPE_AVATAR, id: accountID, @@ -276,7 +276,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { const dismissError = useCallback( (item: ListItem) => { - Report.clearAddRoomMemberError(report.reportID, String(item.accountID)); + clearAddRoomMemberError(report.reportID, String(item.accountID)); }, [report.reportID], ); @@ -285,7 +285,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { if (!report?.policyID || policies === null) { return false; } - return PolicyUtils.isPolicyEmployee(report.policyID, policies); + return isPolicyEmployeeUtils(report.policyID, policies); }, [report?.policyID, policies]); const headerMessage = searchValue.trim() && !data.length ? `${translate('roomMembersPage.memberNotFound')} ${translate('roomMembersPage.useInviteButton')}` : ''; @@ -295,7 +295,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { { text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, - icon: Expensicons.RemoveMembers, + icon: RemoveMembers, onSelected: () => setRemoveMembersConfirmModalVisible(true), }, ]; @@ -322,7 +322,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { success onPress={inviteUser} text={translate('workspace.invite.member')} - icon={Expensicons.Plus} + icon={Plus} innerStyles={[shouldUseNarrowLayout && styles.alignItemsCenter]} style={[shouldUseNarrowLayout && styles.flexGrow1]} /> @@ -367,9 +367,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { testID={RoomMembersPage.displayName} > { Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID, backTo)); @@ -377,7 +375,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { > { if (selectionMode?.isEnabled) { setSelectedMembers([]); @@ -398,7 +396,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { onCancel={() => setRemoveMembersConfirmModalVisible(false)} prompt={translate('roomMembersPage.removeMembersPrompt', { count: selectedMembers.length, - memberName: formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? ''), + memberName: formatPhoneNumber(getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? ''), })} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} @@ -418,9 +416,9 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) { onCheckboxPress={(item) => toggleUser(item)} onSelectRow={openRoomMemberDetails} onSelectAll={() => toggleAllUsers(data)} - showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(personalDetails) || !didLoadRoomMembers} + showLoadingPlaceholder={!isPersonalDetailsReady(personalDetails) || !didLoadRoomMembers} showScrollIndicator - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.mt3]} customListHeader={customListHeader} ListItem={TableListItem} diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index f2a3b9581603..3ad27db440d7 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -12,8 +12,8 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; -import * as Expensicons from '@components/Icon/Expensicons'; -import * as Illustrations from '@components/Icon/Illustrations'; +import {Download, FallbackAvatar, MakeAdmin, Plus, RemoveMembers, Table, User, UserEye} from '@components/Icon/Expensicons'; +import {ReceiptWrangler} from '@components/Icon/Illustrations'; import MessagesRow from '@components/MessagesRow'; import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; @@ -31,19 +31,29 @@ import useSearchBackPress from '@hooks/useSearchBackPress'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import { + clearAddMemberError, + clearDeleteMemberError, + clearInviteDraft, + clearWorkspaceOwnerChangeFlow, + downloadMembersCSV, + isApprover, + openWorkspaceMembersPage, + removeMembers, + updateWorkspaceMembersRole, +} from '@libs/actions/Policy/Member'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {formatPhoneNumber as formatPhoneNumberUtil} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; +import {getMemberAccountIDsForWorkspace, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy, isPolicyAdmin as isPolicyAdminUtils} from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; -import * as Modal from '@userActions/Modal'; -import * as Member from '@userActions/Policy/Member'; -import * as Policy from '@userActions/Policy/Policy'; +import {close} from '@userActions/Modal'; +import {dismissAddedWithPrimaryLoginMessages} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -70,7 +80,7 @@ function invertObject(object: Record): Record { type MemberOption = Omit & {accountID: number}; function WorkspaceMembersPage({personalDetails, route, policy, currentUserPersonalDetails}: WorkspaceMembersPageProps) { - const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); + const policyMemberEmailsToAccountIDs = useMemo(() => getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); const styles = useThemeStyles(); const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); @@ -90,9 +100,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const isPolicyAdmin = isPolicyAdminUtils(policy); const isLoading = useMemo( - () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), + () => !isOfflineAndNoMemberDataAvailable && (!isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), [isOfflineAndNoMemberDataAvailable, personalDetails, policy?.employeeList], ); @@ -107,11 +117,11 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const canSelectMultiple = isPolicyAdmin && (shouldUseNarrowLayout ? selectionMode?.isEnabled : true); const confirmModalPrompt = useMemo(() => { - const approverAccountID = selectedEmployees.find((selectedEmployee) => Member.isApprover(policy, selectedEmployee)); + const approverAccountID = selectedEmployees.find((selectedEmployee) => isApprover(policy, selectedEmployee)); if (!approverAccountID) { return translate('workspace.people.removeMembersPrompt', { count: selectedEmployees.length, - memberName: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''), + memberName: formatPhoneNumberUtil(getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''), }); } return translate('workspace.people.removeMembersWarningPrompt', { @@ -134,7 +144,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson * Get members for the current workspace */ const getWorkspaceMembers = useCallback(() => { - Member.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList))); + openWorkspaceMembersPage(route.params.policyID, Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList))); }, [route.params.policyID, policy?.employeeList]); /** @@ -180,7 +190,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return res?.accountID ?? id; }); - const currentSelectedElements = Object.entries(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList)) + const currentSelectedElements = Object.entries(getMemberAccountIDsForWorkspace(policy?.employeeList)) .filter((employee) => policy?.employeeList?.[employee[0]]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) .map((employee) => employee[1]); @@ -202,7 +212,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson * Open the modal to invite a user */ const inviteUser = useCallback(() => { - Member.clearInviteDraft(route.params.policyID); + clearInviteDraft(route.params.policyID); Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID, Navigation.getActiveRouteWithoutParams())); }, [route.params.policyID]); @@ -220,7 +230,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson setSelectedEmployees([]); setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { - Member.removeMembers(accountIDsToRemove, route.params.policyID); + removeMembers(accountIDsToRemove, route.params.policyID); }); }; @@ -299,11 +309,11 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson /** Opens the member details page */ const openMemberDetails = useCallback( (item: MemberOption) => { - if (!isPolicyAdmin || !PolicyUtils.isPaidGroupPolicy(policy)) { + if (!isPolicyAdmin || !isPaidGroupPolicy(policy)) { Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); return; } - Member.clearWorkspaceOwnerChangeFlow(policyID); + clearWorkspaceOwnerChangeFlow(policyID); Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID)); }, [isPolicyAdmin, policy, policyID, route.params.policyID], @@ -315,9 +325,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const dismissError = useCallback( (item: MemberOption) => { if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - Member.clearDeleteMemberError(route.params.policyID, item.accountID); + clearDeleteMemberError(route.params.policyID, item.accountID); } else { - Member.clearAddMemberError(route.params.policyID, item.accountID); + clearAddMemberError(route.params.policyID, item.accountID); } }, [route.params.policyID], @@ -331,7 +341,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { + if (isDeletedPolicyEmployee(policyEmployee, isOffline)) { return; } @@ -345,8 +355,8 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they // see random people added to their policy, but guides having access to the policies help set them up. - if (PolicyUtils.isExpensifyTeam(details?.login ?? details?.displayName)) { - if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) { + if (isExpensifyTeam(details?.login ?? details?.displayName)) { + if (policyOwner && currentUserLogin && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) { return; } } @@ -372,12 +382,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson isDisabled: isPendingDeleteOrError, isInteractive: !details.isOptimisticPersonalDetail, cursorStyle: details.isOptimisticPersonalDetail ? styles.cursorDefault : {}, - text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + text: formatPhoneNumber(getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, icons: [ { - source: details.avatar ?? Expensicons.FallbackAvatar, + source: details.avatar ?? FallbackAvatar, name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, @@ -389,7 +399,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = OptionsListUtils.sortAlphabetically(result, 'text'); + result = sortAlphabetically(result, 'text'); return result; }, [ isOffline, @@ -421,7 +431,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson } const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String); selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails); - Member.clearInviteDraft(route.params.policyID); + clearInviteDraft(route.params.policyID); }, [invitedEmailsToAccountIDsDraft, isFocused, accountIDs, prevAccountIDs, route.params.policyID]); const getHeaderMessage = () => { @@ -441,7 +451,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // eslint-disable-next-line @typescript-eslint/naming-convention messages={{0: translate('workspace.people.addedWithPrimary')}} containerStyles={[styles.pb5, styles.ph5]} - onClose={() => Policy.dismissAddedWithPrimaryLoginMessages(policyID)} + onClose={() => dismissAddedWithPrimaryLoginMessages(policyID)} /> )} @@ -480,7 +490,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return policy?.employeeList?.[email]?.role !== role; }); - Member.updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); + updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); setSelectedEmployees([]); }; @@ -489,12 +499,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson { text: translate('workspace.people.removeMembersTitle', {count: selectedEmployees.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, - icon: Expensicons.RemoveMembers, + icon: RemoveMembers, onSelected: askForConfirmationToRemove, }, ]; - if (!PolicyUtils.isPaidGroupPolicy(policy)) { + if (!isPaidGroupPolicy(policy)) { return options; } @@ -506,20 +516,20 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const memberOption = { text: translate('workspace.people.makeMember'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, - icon: Expensicons.User, + icon: User, onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER), }; const adminOption = { text: translate('workspace.people.makeAdmin'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, - icon: Expensicons.MakeAdmin, + icon: MakeAdmin, onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN), }; const auditorOption = { text: translate('workspace.people.makeAuditor'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_AUDITOR, - icon: Expensicons.UserEye, + icon: UserEye, onSelected: () => changeUserRole(CONST.POLICY.ROLE.AUDITOR), }; @@ -563,7 +573,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson success onPress={inviteUser} text={translate('workspace.invite.member')} - icon={Expensicons.Plus} + icon={Plus} innerStyles={[shouldUseNarrowLayout && styles.alignItemsCenter]} style={[shouldUseNarrowLayout && styles.flexGrow1, shouldUseNarrowLayout && styles.mb3]} /> @@ -577,27 +587,27 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const menuItems = [ { - icon: Expensicons.Table, + icon: Table, text: translate('spreadsheet.importSpreadsheet'), onSelected: () => { if (isOffline) { - Modal.close(() => setIsOfflineModalVisible(true)); + close(() => setIsOfflineModalVisible(true)); return; } Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID)); }, }, { - icon: Expensicons.Download, + icon: Download, text: translate('spreadsheet.downloadCSV'), onSelected: () => { if (isOffline) { - Modal.close(() => setIsOfflineModalVisible(true)); + close(() => setIsOfflineModalVisible(true)); return; } - Modal.close(() => { - Member.downloadMembersCSV(policyID, () => { + close(() => { + downloadMembersCSV(policyID, () => { setIsDownloadFailureModalVisible(true); }); }); @@ -615,7 +625,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson headerText={selectionModeHeader ? translate('common.selectMultiple') : translate('workspace.common.members')} route={route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS} - icon={!selectionModeHeader ? Illustrations.ReceiptWrangler : undefined} + icon={!selectionModeHeader ? ReceiptWrangler : undefined} headerContent={!shouldUseNarrowLayout && getHeaderButtons()} testID={WorkspaceMembersPage.displayName} shouldShowLoading={false} @@ -690,7 +700,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} showLoadingPlaceholder={isLoading} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} textInputRef={textInputRef} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index da3522d397b6..fcf01d9df4ef 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -29,6 +29,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -49,7 +50,6 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {PolicyCategory} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import useSearchBackPress from '@hooks/useSearchBackPress'; type PolicyOption = ListItem & { /** Category name is used as a key for the selectedCategories state */ From f7ce19899c8a0233ac5817ee23edca97697c6b71 Mon Sep 17 00:00:00 2001 From: Ming Date: Sat, 1 Feb 2025 18:38:54 +0700 Subject: [PATCH 057/201] add listener remove --- src/hooks/useSearchBackPress/index.android.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSearchBackPress/index.android.ts b/src/hooks/useSearchBackPress/index.android.ts index 3063a48116f2..51fa67ce78ba 100644 --- a/src/hooks/useSearchBackPress/index.android.ts +++ b/src/hooks/useSearchBackPress/index.android.ts @@ -19,8 +19,8 @@ const useSearchBackPress: UseSearchBackPress = ({onClearSelection, onNavigationC onNavigationCallBack(); return false; }; - BackHandler.addEventListener('hardwareBackPress', onBackPress); - return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress); + const backHandler = BackHandler.addEventListener('hardwareBackPress', onBackPress); + return () => backHandler.remove(); }, [selectionMode, onClearSelection, onNavigationCallBack]), ); }; From c4499b3246907ab607044782c45abe1f22bc1bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=8E=E4=BA=AE=E7=9A=84?= Date: Sat, 1 Feb 2025 18:46:12 +0700 Subject: [PATCH 058/201] Update src/hooks/useSearchBackPress/index.android.ts Co-authored-by: Hans --- src/hooks/useSearchBackPress/index.android.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSearchBackPress/index.android.ts b/src/hooks/useSearchBackPress/index.android.ts index 51fa67ce78ba..32aa7bb5b87a 100644 --- a/src/hooks/useSearchBackPress/index.android.ts +++ b/src/hooks/useSearchBackPress/index.android.ts @@ -21,7 +21,7 @@ const useSearchBackPress: UseSearchBackPress = ({onClearSelection, onNavigationC }; const backHandler = BackHandler.addEventListener('hardwareBackPress', onBackPress); return () => backHandler.remove(); - }, [selectionMode, onClearSelection, onNavigationCallBack]), + }, [selectionMode?.isEnabled, onClearSelection, onNavigationCallBack]), ); }; From 988cc82bcc176d4b209efb48df153144be8a8698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=8E=E4=BA=AE=E7=9A=84?= Date: Sat, 1 Feb 2025 18:46:30 +0700 Subject: [PATCH 059/201] Update src/hooks/useSearchBackPress/types.ts Co-authored-by: Hans --- src/hooks/useSearchBackPress/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/useSearchBackPress/types.ts b/src/hooks/useSearchBackPress/types.ts index 88a2aa0d5964..64b1a440b1cf 100644 --- a/src/hooks/useSearchBackPress/types.ts +++ b/src/hooks/useSearchBackPress/types.ts @@ -1,7 +1,6 @@ type UseSearchBackPressParams = { onClearSelection: () => void; onNavigationCallBack: () => void; - backTo?: string; }; type UseSearchBackPress = (params: UseSearchBackPressParams) => void; From 02c54c35a460553d4426f51ab1fa543f93ea5d33 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Sun, 2 Feb 2025 00:19:47 +0700 Subject: [PATCH 060/201] fix background hover --- src/pages/home/report/PureReportActionItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 5a5eef58b62c..c36391ecec70 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1010,7 +1010,7 @@ function PureReportActionItem({ childReportID={`${action.childReportID}`} numberOfReplies={numberOfThreadReplies} mostRecentReply={`${action.childLastVisibleActionCreated}`} - isHovered={hovered} + isHovered={hovered || isContextMenuActive} icons={getIconsForParticipants(oldestFourAccountIDs, personalDetails)} onSecondaryInteraction={showPopover} isActive={isReportActionActive && !isContextMenuActive} @@ -1045,7 +1045,7 @@ function PureReportActionItem({ shouldShowSubscriptAvatar={shouldShowSubscriptAvatar} report={report} iouReport={iouReport} - isHovered={hovered} + isHovered={hovered || isContextMenuActive} isActive={isReportActionActive && !isContextMenuActive} hasBeenFlagged={ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && !isPendingRemove(action) From 7b55bf539911900a559c652e00613674865423b4 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 2 Feb 2025 20:02:35 +0530 Subject: [PATCH 061/201] Fixed submit button layout in Company card transaction start date page --- .../assignCard/TransactionStartDateSelectorModal.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx index 8c0990800bb4..5e5e258912a8 100644 --- a/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx +++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx @@ -51,7 +51,6 @@ function TransactionStartDateSelectorModal({isVisible, date, handleSelectDate, o Date: Sun, 2 Feb 2025 20:13:09 +0530 Subject: [PATCH 062/201] Fix lint --- .../assignCard/TransactionStartDateSelectorModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx index 5e5e258912a8..1b2ba15bdbd7 100644 --- a/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx +++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx @@ -8,7 +8,7 @@ import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ValidationUtils from '@libs/ValidationUtils'; +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/AssignCardForm'; @@ -32,7 +32,7 @@ function TransactionStartDateSelectorModal({isVisible, date, handleSelectDate, o const {translate} = useLocalize(); const validate = (values: FormOnyxValues): FormInputErrors => - ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.START_DATE]); + getFieldRequiredErrors(values, [INPUT_IDS.START_DATE]); const submit = (values: FormOnyxValues) => { handleSelectDate(values[INPUT_IDS.START_DATE]); From 146b89e5caa812bee780355cab67ecb33a4651fa Mon Sep 17 00:00:00 2001 From: M00rish Date: Sun, 2 Feb 2025 20:36:41 +0100 Subject: [PATCH 063/201] refactor to show not found page only when navigating to a deleted Action --- src/pages/home/ReportScreen.tsx | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index c1b809c932b6..11737aae66b5 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -387,7 +387,11 @@ function ReportScreen({route, navigation}: ReportScreenProps) { [linkedAction, report], ); - const [isClearingDeletedLinkedAction, setIsClearingDeletedLinkedAction] = useState(); + const previsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); + + const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); + + const [isNavigatingToDeletedMessage, setIsNavigatingToDeletedMessage] = useState(false); const isLinkedActionInaccessibleWhisper = useMemo( () => !!linkedAction && isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), @@ -420,7 +424,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundLinkedAction = - (!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && isClearingDeletedLinkedAction === false) || + (!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && isNavigatingToDeletedMessage) || (shouldShowSkeleton && !reportMetadata.isLoadingInitialReportActions && !!reportActionIDFromRoute && @@ -735,23 +739,23 @@ function ReportScreen({route, navigation}: ReportScreenProps) { }, [fetchReport]); useEffect(() => { - - if (!isLinkedActionDeleted || isClearingDeletedLinkedAction) - { + // Only handle deletion cases when there's a deleted action + if (!isLinkedActionDeleted) { + setIsNavigatingToDeletedMessage(false); return; } - Navigation.setParams({reportActionID: ''}); - setIsClearingDeletedLinkedAction(true); - }, [isLinkedActionDeleted, isClearingDeletedLinkedAction]); - - - useEffect(() => { - if (!isClearingDeletedLinkedAction || reportActionIDFromRoute) { + // Set navigation state when user clicks a deleted message link + if (lastReportActionIDFromRoute !== reportActionIDFromRoute) { + setIsNavigatingToDeletedMessage(true); return; } - setIsClearingDeletedLinkedAction(false); - }, [isClearingDeletedLinkedAction, reportActionIDFromRoute]); + + // Clear params when message gets deleted while viewing + if (!isNavigatingToDeletedMessage && previsLinkedActionDeleted === false) { + Navigation.setParams({reportActionID: ''}); + } + }, [isLinkedActionDeleted, previsLinkedActionDeleted, lastReportActionIDFromRoute, reportActionIDFromRoute, isNavigatingToDeletedMessage]); // If user redirects to an inaccessible whisper via a deeplink, on a report they have access to, // then we set reportActionID as empty string, so we display them the report and not the "Not found page". @@ -785,7 +789,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) { !isDeletedAction(mostRecentReportAction); const lastRoute = usePrevious(route); - const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); const onComposerFocus = useCallback(() => setIsComposerFocus(true), []); const onComposerBlur = useCallback(() => setIsComposerFocus(false), []); From 72da60199a4785953921c3003ade0dfa5c846f6f Mon Sep 17 00:00:00 2001 From: M00rish Date: Sun, 2 Feb 2025 20:47:25 +0100 Subject: [PATCH 064/201] improve naming --- src/pages/home/ReportScreen.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 11737aae66b5..4cf7e0ca6ee3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -388,10 +388,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ); const previsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); - + const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); - const [isNavigatingToDeletedMessage, setIsNavigatingToDeletedMessage] = useState(false); + const [isNavigatingToAction, setIsNavigatingToAction] = useState(false); const isLinkedActionInaccessibleWhisper = useMemo( () => !!linkedAction && isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), @@ -424,7 +424,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundLinkedAction = - (!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && isNavigatingToDeletedMessage) || + (!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted && isNavigatingToAction) || (shouldShowSkeleton && !reportMetadata.isLoadingInitialReportActions && !!reportActionIDFromRoute && @@ -741,21 +741,21 @@ function ReportScreen({route, navigation}: ReportScreenProps) { useEffect(() => { // Only handle deletion cases when there's a deleted action if (!isLinkedActionDeleted) { - setIsNavigatingToDeletedMessage(false); + setIsNavigatingToAction(false); return; } - // Set navigation state when user clicks a deleted message link + // Set navigation state when user clicks a deleted Action link if (lastReportActionIDFromRoute !== reportActionIDFromRoute) { - setIsNavigatingToDeletedMessage(true); + setIsNavigatingToAction(true); return; } - // Clear params when message gets deleted while viewing - if (!isNavigatingToDeletedMessage && previsLinkedActionDeleted === false) { + // Clear params when Action gets deleted while heighlighted + if (!isNavigatingToAction && previsLinkedActionDeleted === false) { Navigation.setParams({reportActionID: ''}); } - }, [isLinkedActionDeleted, previsLinkedActionDeleted, lastReportActionIDFromRoute, reportActionIDFromRoute, isNavigatingToDeletedMessage]); + }, [isLinkedActionDeleted, previsLinkedActionDeleted, lastReportActionIDFromRoute, reportActionIDFromRoute, isNavigatingToAction]); // If user redirects to an inaccessible whisper via a deeplink, on a report they have access to, // then we set reportActionID as empty string, so we display them the report and not the "Not found page". From 0adb14c6002d568a6ea759e72322198e5327fbeb Mon Sep 17 00:00:00 2001 From: OmarKoueifi Date: Mon, 3 Feb 2025 03:31:37 -0500 Subject: [PATCH 065/201] Prevent It's not here message when logging in #55721 --- src/libs/actions/Report.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b77b8115cb59..024d23762aa1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -68,6 +68,7 @@ import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import {registerPaginationConfig} from '@libs/Middleware/Pagination'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import shouldOpenOnAdminRoom from '@libs/Navigation/shouldOpenOnAdminRoom'; import {isOnboardingFlowName} from '@libs/NavigationUtils'; import enhanceParameters from '@libs/Network/enhanceParameters'; import type {NetworkStatus} from '@libs/NetworkConnection'; @@ -2942,6 +2943,20 @@ function openReportFromDeepLink(url: string) { return; } + // Check if the report exists in the collection + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + // If the report does not exist, navigate to the last accessed report or Concierge chat + if (!report) { + const lastAccessedReportID = findLastAccessedReport(false, shouldOpenOnAdminRoom(), undefined, reportID)?.reportID; + if (lastAccessedReportID) { + const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); + Navigation.navigate(lastAccessedReportRoute, CONST.NAVIGATION.ACTION_TYPE.PUSH); + return; + } + navigateToConciergeChat(false, () => true, CONST.NAVIGATION.ACTION_TYPE.PUSH); + return; + } + Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH); }; From 68b3262906132b97b367e102d50a4564ad073c4c Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Mon, 3 Feb 2025 16:24:26 +0100 Subject: [PATCH 066/201] fix merge --- .../Search/SearchAutocompleteInput.tsx | 16 +++------------- src/libs/SearchAutocompleteUtils.ts | 9 ++------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 0c2dd8124b46..6a3c9c8c5e5e 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -2,11 +2,9 @@ import isEqual from 'lodash/isEqual'; import type {ForwardedRef, ReactNode, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; -import {useSharedValue} from 'react-native-reanimated'; import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import {useSharedValue} from 'react-native-reanimated'; import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; @@ -101,7 +99,6 @@ function SearchAutocompleteInput( const {isOffline} = useNetwork(); const {activeWorkspaceID} = useActiveWorkspace(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const personalDetailsSharedValue = useSharedValue({login: currentUserPersonalDetails.login, userDisplayName: currentUserPersonalDetails.displayName}); const lastMap = useRef({}); const [map, setMap] = useState({}); @@ -133,13 +130,6 @@ function SearchAutocompleteInput( const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - useEffect(() => { - runOnLiveMarkdownRuntime(() => { - 'worklet'; - - personalDetailsSharedValue.set({login: currentUserPersonalDetails.login, userDisplayName: currentUserPersonalDetails.displayName}); - })(); - }, [currentUserPersonalDetails, personalDetailsSharedValue]); useEffect(() => { runOnLiveMarkdownRuntime(() => { 'worklet'; @@ -166,9 +156,9 @@ function SearchAutocompleteInput( (input: string) => { 'worklet'; - return parseForLiveMarkdown(input, emailList, personalDetailsSharedValue, map, currencySharedValue, categorySharedValue, tagSharedValue); + return parseForLiveMarkdown(input, emailList, currentUserPersonalDetails.displayName ?? '', map, currencySharedValue, categorySharedValue, tagSharedValue); }, - [personalDetailsSharedValue, map, currencySharedValue, categorySharedValue, tagSharedValue, emailList], + [currentUserPersonalDetails.displayName, map, currencySharedValue, categorySharedValue, tagSharedValue, emailList], ); const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index fc5836a2c1da..e798e88a5514 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -143,7 +143,7 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { function parseForLiveMarkdown( input: string, userLogins: string[], - details: SharedValue<{login: string | undefined; userDisplayName: string | undefined}>, + userDisplayName: string, map: SubstitutionMap, currencyList: SharedValue, categoryList: SharedValue, @@ -177,12 +177,7 @@ function parseForLiveMarkdown( .filter((range) => range.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG || tagList.get().includes(range.value)) .map((range) => { let type = 'mention-user'; - if ( - (range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && - (range.value === details.get().login || range.value === details.get().userDisplayName) - ) { - type = 'mention-here'; - } + if (range.key === 'syntax') { type = 'syntax'; } From 50ab21a9481e1b7ae17d081cec9636374be998b7 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 3 Feb 2025 16:27:42 +0100 Subject: [PATCH 067/201] Refactor dependencies in ReportScreen hooks for improved performance --- src/pages/home/ReportScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index ea16d28a47ac..dfa060d8be08 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -529,7 +529,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } fetchReport(); - }, [reportIDFromRoute, isLoadingApp, report, fetchReport, reportMetadata]); + }, [reportIDFromRoute, isLoadingApp, report, fetchReport, reportMetadata.isOptimisticReport]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -576,7 +576,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } fetchReportIfNeeded(); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isLoadingReportOnyx, fetchReportIfNeeded]); + }, [isLoadingReportOnyx]); useEffect(() => { if (isLoadingReportOnyx || !reportActionIDFromRoute || isLinkedMessagePageReady) { From d2f0a5033be9180bd7f31309b22205ef7f12edd4 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 3 Feb 2025 22:41:23 +0700 Subject: [PATCH 068/201] fix: chat gbr flickers when marking task as done --- src/libs/actions/Task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 2864b989ed3d..7132e870ed70 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -352,7 +352,7 @@ function getOutstandingChildTask(taskReport: OnyxEntry) { reportAction.childType === CONST.REPORT.TYPE.TASK && reportAction?.childStateNum === CONST.REPORT.STATE_NUM.OPEN && reportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN && - ReportActionsUtils.getReportActionMessage(reportAction)?.isDeletedParentAction + !ReportActionsUtils.getReportActionMessage(reportAction)?.isDeletedParentAction ) { return true; } From d5acdeee562539eef52d46ef60ab8d9bcd220e64 Mon Sep 17 00:00:00 2001 From: 289Adam289 Date: Mon, 3 Feb 2025 17:14:44 +0100 Subject: [PATCH 069/201] make emailList shared value --- .../Search/SearchAutocompleteInput.tsx | 21 ++++++++++++++----- src/libs/SearchAutocompleteUtils.ts | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 6a3c9c8c5e5e..ab6ecd333dfe 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -118,6 +118,10 @@ function SearchAutocompleteInput( }, [activeWorkspaceID, allPoliciesTags]); const tagSharedValue = useSharedValue(tagAutocompleteList); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const emailList = Object.keys(loginList ?? {}); + const emailListSharedValue = useSharedValue(emailList); + useEffect(() => { if (lastMap.current && !isEqual(lastMap.current, substitutionMap)) { lastMap.current = substitutionMap; @@ -125,11 +129,16 @@ function SearchAutocompleteInput( setMap(lastMap.current ?? {}); }, [substitutionMap, lastMap]); - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); - const emailList = Object.keys(loginList ?? {}); - const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + useEffect(() => { + runOnLiveMarkdownRuntime(() => { + 'worklet'; + + emailListSharedValue.set(emailList); + })(); + }, [emailList, emailListSharedValue]); + useEffect(() => { runOnLiveMarkdownRuntime(() => { 'worklet'; @@ -137,6 +146,7 @@ function SearchAutocompleteInput( currencySharedValue.set(currencyAutocompleteList); })(); }, [currencyAutocompleteList, currencySharedValue]); + useEffect(() => { runOnLiveMarkdownRuntime(() => { 'worklet'; @@ -144,6 +154,7 @@ function SearchAutocompleteInput( categorySharedValue.set(categoryAutocompleteList); })(); }, [categorySharedValue, categoryAutocompleteList]); + useEffect(() => { runOnLiveMarkdownRuntime(() => { 'worklet'; @@ -156,9 +167,9 @@ function SearchAutocompleteInput( (input: string) => { 'worklet'; - return parseForLiveMarkdown(input, emailList, currentUserPersonalDetails.displayName ?? '', map, currencySharedValue, categorySharedValue, tagSharedValue); + return parseForLiveMarkdown(input, currentUserPersonalDetails.displayName ?? '', map, emailListSharedValue, currencySharedValue, categorySharedValue, tagSharedValue); }, - [currentUserPersonalDetails.displayName, map, currencySharedValue, categorySharedValue, tagSharedValue, emailList], + [currentUserPersonalDetails.displayName, map, currencySharedValue, categorySharedValue, tagSharedValue, emailListSharedValue], ); const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index e798e88a5514..73b35ac0ed21 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -142,9 +142,9 @@ function getAutocompleteQueryWithComma(prevQuery: string, newQuery: string) { */ function parseForLiveMarkdown( input: string, - userLogins: string[], userDisplayName: string, map: SubstitutionMap, + userLogins: SharedValue, currencyList: SharedValue, categoryList: SharedValue, tagList: SharedValue, @@ -182,7 +182,7 @@ function parseForLiveMarkdown( type = 'syntax'; } - if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (userLogins.includes(range.value) || range.value === userDisplayName)) { + if ((range.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO || CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM) && (userLogins.get().includes(range.value) || range.value === userDisplayName)) { type = 'mention-here'; } From d18f99b66235cee8266d0ac9d0c1ec691b8898c1 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 28 Jan 2025 13:38:12 +0200 Subject: [PATCH 070/201] Pass policy to Travel.bookATrip --- src/libs/actions/Travel.ts | 17 +++++++---------- src/pages/Search/EmptySearchView.tsx | 10 +++++++++- src/pages/Travel/ManageTrips.tsx | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 1886885587c4..8605ce924c33 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -14,7 +14,7 @@ import {getAdminsPrivateEmailDomains, getPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {TravelSettings} from '@src/types/onyx'; +import type {Policy, TravelSettings} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {buildTravelDotURL, openTravelDotLink} from './Link'; @@ -114,22 +114,19 @@ function provisionDomain(domain: string) { Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); } -function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch>, ctaErrorMessage = ''): void { - if (!activePolicyID) { - return; - } +function bookATrip(policy: Policy, translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch>, ctaErrorMessage = ''): void { if (Str.isSMSLogin(primaryLogin)) { setCtaErrorMessage(translate('travel.phoneError')); return; } - const policy = getPolicy(activePolicyID); - if (isEmptyObject(policy?.address)) { - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(activePolicyID, Navigation.getActiveRoute())); + + if (isEmptyObject(policy.address)) { + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy.id, Navigation.getActiveRoute())); return; } - const isPolicyProvisioned = policy?.travelSettings?.spotnanaCompanyID ?? policy?.travelSettings?.associatedTravelDomainAccountID; - if (policy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)) { + const isPolicyProvisioned = policy.travelSettings?.spotnanaCompanyID ?? policy.travelSettings?.associatedTravelDomainAccountID; + if (policy.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)) { openTravelDotLink(activePolicyID) ?.then(() => { if (!NativeModules.HybridAppModule || !isSingleNewDotEntry) { diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index c27edc2e70e2..17b57aeec855 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -60,6 +60,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { const shouldRedirectToExpensifyClassic = useMemo(() => { return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); }, [allPolicies]); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [ctaErrorMessage, setCtaErrorMessage] = useState(''); @@ -133,7 +134,13 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { buttons: [ { buttonText: translate('search.searchResults.emptyTripResults.buttonText'), - buttonAction: () => bookATrip(translate, setCtaErrorMessage, ctaErrorMessage), + buttonAction: () => { + const activePolicy = ((allPolicies as OnyxCollection) ?? {})[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; + if (!activePolicy) { + return; + } + bookATrip(activePolicy, translate, setCtaErrorMessage, ctaErrorMessage); + }, success: true, }, ], @@ -245,6 +252,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { viewTourTaskReport, canModifyTheTask, canActionTheTask, + activePolicyID, ]); return ( diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx index ac427d1d56c9..bbfd2223b48f 100644 --- a/src/pages/Travel/ManageTrips.tsx +++ b/src/pages/Travel/ManageTrips.tsx @@ -55,7 +55,7 @@ function ManageTrips() { ctaText={translate('travel.bookTravel')} ctaAccessibilityLabel={translate('travel.bookTravel')} onCtaPress={() => { - bookATrip(translate, setCtaErrorMessage, ctaErrorMessage); + bookATrip(policy, translate, setCtaErrorMessage, ctaErrorMessage); }} ctaErrorMessage={ctaErrorMessage} illustration={LottieAnimations.TripsEmptyState} From 9ae5e5599c4ec35fe6c31cd46b381111e6106c32 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 28 Jan 2025 13:47:44 +0200 Subject: [PATCH 071/201] Remove activePolicyID from Travel.ts --- src/libs/actions/Travel.ts | 12 ++---------- src/pages/Search/EmptySearchView.tsx | 3 ++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 8605ce924c33..cf0f617a0617 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -10,7 +10,7 @@ import {WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import {getAdminsPrivateEmailDomains, getPolicy} from '@libs/PolicyUtils'; +import {getAdminsPrivateEmailDomains} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -26,14 +26,6 @@ Onyx.connect({ }, }); -let activePolicyID: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, - callback: (val) => { - activePolicyID = val; - }, -}); - let primaryLogin: string; Onyx.connect({ key: ONYXKEYS.ACCOUNT, @@ -127,7 +119,7 @@ function bookATrip(policy: Policy, translate: LocaleContextProps['translate'], s const isPolicyProvisioned = policy.travelSettings?.spotnanaCompanyID ?? policy.travelSettings?.associatedTravelDomainAccountID; if (policy.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)) { - openTravelDotLink(activePolicyID) + openTravelDotLink(policy.id) ?.then(() => { if (!NativeModules.HybridAppModule || !isSingleNewDotEntry) { return; diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 17b57aeec855..6040ae0a63d6 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -135,7 +135,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { { buttonText: translate('search.searchResults.emptyTripResults.buttonText'), buttonAction: () => { - const activePolicy = ((allPolicies as OnyxCollection) ?? {})[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; + const activePolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; if (!activePolicy) { return; } @@ -252,6 +252,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { viewTourTaskReport, canModifyTheTask, canActionTheTask, + allPolicies, activePolicyID, ]); From 1a32608deabb24e640ad162118466e31d04b2295 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 28 Jan 2025 15:11:13 +0200 Subject: [PATCH 072/201] Remove primaryLogin from Travel.ts --- src/libs/actions/Travel.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index cf0f617a0617..8a5a51daa621 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -11,6 +11,7 @@ import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {getAdminsPrivateEmailDomains} from '@libs/PolicyUtils'; +import {getContactMethod} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -26,14 +27,6 @@ Onyx.connect({ }, }); -let primaryLogin: string; -Onyx.connect({ - key: ONYXKEYS.ACCOUNT, - callback: (val) => { - primaryLogin = val?.primaryLogin ?? ''; - }, -}); - let isSingleNewDotEntry: boolean | undefined; Onyx.connect({ key: ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY, @@ -107,7 +100,7 @@ function provisionDomain(domain: string) { } function bookATrip(policy: Policy, translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch>, ctaErrorMessage = ''): void { - if (Str.isSMSLogin(primaryLogin)) { + if (Str.isSMSLogin(getContactMethod())) { setCtaErrorMessage(translate('travel.phoneError')); return; } From bc569f509e83c3ceeeeae7aeb9bbd08a4c22118f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 28 Jan 2025 23:52:37 +0200 Subject: [PATCH 073/201] Add footer to the FeatureList component --- src/components/FeatureList.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/FeatureList.tsx b/src/components/FeatureList.tsx index 70c56ad5d963..1964faddb4a1 100644 --- a/src/components/FeatureList.tsx +++ b/src/components/FeatureList.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import {View} from 'react-native'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -65,6 +65,9 @@ type FeatureListProps = { /** Padding for content on large screens */ contentPaddingOnLargeScreens?: {padding: number}; + + /** Custom content to display in the footer */ + footer?: ReactNode; }; function FeatureList({ @@ -84,6 +87,7 @@ function FeatureList({ illustrationContainerStyle, titleStyles, contentPaddingOnLargeScreens, + footer, }: FeatureListProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -146,6 +150,7 @@ function FeatureList({ success large /> + {!!footer && <>{footer}} ); From d16741c8de67cbed7947efb056fc8ac533959b61 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Jan 2025 00:05:12 +0200 Subject: [PATCH 074/201] Move Travel specific components out of FeatureList --- src/components/BookTravelButton.tsx | 37 +++++++++++++++++++++++++++ src/components/FeatureList.tsx | 39 ++++++++--------------------- src/pages/Travel/ManageTrips.tsx | 17 ++++++++++--- 3 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 src/components/BookTravelButton.tsx diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx new file mode 100644 index 000000000000..3362f23b615e --- /dev/null +++ b/src/components/BookTravelButton.tsx @@ -0,0 +1,37 @@ +import React, {useState} from 'react'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Button from './Button'; +import DotIndicatorMessage from './DotIndicatorMessage'; + +type BookTravelButtonProps = { + text: string; +}; + +function BookTravelButton({text}: BookTravelButtonProps) { + const [errorMessage, setErrorMessage] = useState(''); + const styles = useThemeStyles(); + + return ( + <> + {!!errorMessage && ( + + )} +