From bb4ce473435efb3e5def3c75b07dea516aeaeea3 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 15 Feb 2024 04:37:42 +0500 Subject: [PATCH 001/189] add policyReportFields to the policy object directly --- src/ONYXKEYS.ts | 2 -- .../ReportActionItem/MoneyReportView.tsx | 9 ++--- src/libs/ReportUtils.ts | 27 ++------------- src/pages/EditReportFieldPage.tsx | 12 ++----- src/pages/home/report/ReportActionItem.js | 6 ---- src/types/onyx/Policy.ts | 33 ++++++++++++++++++- src/types/onyx/PolicyReportField.ts | 30 ----------------- src/types/onyx/index.ts | 3 +- 8 files changed, 42 insertions(+), 80 deletions(-) delete mode 100644 src/types/onyx/PolicyReportField.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5755296f3bb5..07061ab0bfc0 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -278,7 +278,6 @@ const ONYXKEYS = { POLICY_TAGS: 'policyTags_', POLICY_TAX_RATE: 'policyTaxRates_', POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', - POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', REPORT: 'report_', @@ -439,7 +438,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index f0cd8dc1b4b5..bf1980578079 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -28,14 +28,11 @@ type MoneyReportViewProps = { /** Policy that the report belongs to */ policy: Policy; - /** Policy report fields */ - policyReportFields: PolicyReportField[]; - /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, policy, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { +function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -59,9 +56,9 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont ]; const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, policyReportFields); + const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy.reportFields || {})); return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); - }, [policyReportFields, report]); + }, [policy, report]); return ( diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ebde1b1bf8ab..e8066e37467f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -15,20 +15,7 @@ import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languag import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type { - Beta, - PersonalDetails, - PersonalDetailsList, - Policy, - PolicyReportField, - PolicyReportFields, - Report, - ReportAction, - ReportMetadata, - Session, - Transaction, - TransactionViolation, -} from '@src/types/onyx'; +import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type { @@ -463,14 +450,6 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); -let allPolicyReportFields: OnyxCollection = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, - waitForCollectionCallback: true, - callback: (value) => (allPolicyReportFields = value), -}); - let allBetas: OnyxEntry; Onyx.connect({ key: ONYXKEYS.BETAS, @@ -1972,7 +1951,7 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry /** * Given a set of report fields, return the field of type formula */ -function getFormulaTypeReportField(reportFields: PolicyReportFields) { +function getFormulaTypeReportField(reportFields: Record) { return Object.values(reportFields).find((field) => field.type === 'formula'); } @@ -1980,7 +1959,7 @@ function getFormulaTypeReportField(reportFields: PolicyReportFields) { * Get the report fields attached to the policy given policyID */ function getReportFieldsByPolicyID(policyID: string) { - return Object.entries(allPolicyReportFields ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, '') === policyID)?.[1]; + return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.reportFields; } /** diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 4124a9ebef98..015b2cabd51c 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -9,7 +9,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActions from '@src/libs/actions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyReportFields, Report} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import EditReportFieldDatePage from './EditReportFieldDatePage'; import EditReportFieldDropdownPage from './EditReportFieldDropdownPage'; import EditReportFieldTextPage from './EditReportFieldTextPage'; @@ -18,9 +18,6 @@ type EditReportFieldPageOnyxProps = { /** The report object for the expense report */ report: OnyxEntry; - /** Policy report fields */ - policyReportFields: OnyxEntry; - /** Policy to which the report belongs to */ policy: OnyxEntry; }; @@ -42,8 +39,8 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; }; -function EditReportFieldPage({route, policy, report, policyReportFields}: EditReportFieldPageProps) { - const reportField = report?.reportFields?.[route.params.fieldID] ?? policyReportFields?.[route.params.fieldID]; +function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { + const reportField = report?.reportFields?.[route.params.fieldID] ?? policy?.reportFields?.[route.params.fieldID]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { @@ -121,9 +118,6 @@ export default withOnyx( report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, }, - policyReportFields: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${route.params.policyID}`, - }, policy: { key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, }, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 39a5fcaa4ee0..4281adeb3eaa 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -672,7 +672,6 @@ function ReportActionItem(props) { @@ -836,10 +835,6 @@ export default compose( }, initialValue: {}, }, - policyReportFields: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), - initialValue: [], - }, policy: { key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined), initialValue: {}, @@ -886,7 +881,6 @@ export default compose( lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && _.isEqual(prevProps.policy, nextProps.policy), ), diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 7d4c08374b81..46d07a56183c 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -45,6 +45,34 @@ type Connection = { type AutoReportingOffset = number | ValueOf; +type PolicyReportFieldType = 'text' | 'date' | 'dropdown' | 'formula'; + +type PolicyReportField = { + /** Name of the field */ + name: string; + + /** Default value assigned to the field */ + defaultValue: string; + + /** Unique id of the field */ + fieldID: string; + + /** Position at which the field should show up relative to the other fields */ + orderWeight: number; + + /** Type of report field */ + type: PolicyReportFieldType; + + /** Tells if the field is required or not */ + deletable: boolean; + + /** Value of the field */ + value: string; + + /** Options to select from if field is of type dropdown */ + values: string[]; +}; + type Policy = { /** The ID of the policy */ id: string; @@ -179,8 +207,11 @@ type Policy = { /** All the integration connections attached to the policy */ connections?: Record; + + /** Report fields attached to the policy */ + reportFields?: Record; }; export default Policy; -export type {Unit, CustomUnit, Attributes, Rate}; +export type {Unit, CustomUnit, Attributes, Rate, PolicyReportField, PolicyReportFieldType}; diff --git a/src/types/onyx/PolicyReportField.ts b/src/types/onyx/PolicyReportField.ts deleted file mode 100644 index de385070aa25..000000000000 --- a/src/types/onyx/PolicyReportField.ts +++ /dev/null @@ -1,30 +0,0 @@ -type PolicyReportFieldType = 'text' | 'date' | 'dropdown' | 'formula'; - -type PolicyReportField = { - /** Name of the field */ - name: string; - - /** Default value assigned to the field */ - defaultValue: string; - - /** Unique id of the field */ - fieldID: string; - - /** Position at which the field should show up relative to the other fields */ - orderWeight: number; - - /** Type of report field */ - type: PolicyReportFieldType; - - /** Tells if the field is required or not */ - deletable: boolean; - - /** Value of the field */ - value: string; - - /** Options to select from if field is of type dropdown */ - values: string[]; -}; - -type PolicyReportFields = Record; -export type {PolicyReportField, PolicyReportFields}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 1b2ecdbdce12..e87a54ab6623 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -30,10 +30,10 @@ import type {PersonalDetailsList} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; import type Policy from './Policy'; +import type {PolicyReportField} from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; -import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; @@ -143,7 +143,6 @@ export type { WorkspaceRateAndUnit, ReportUserIsTyping, PolicyReportField, - PolicyReportFields, RecentlyUsedReportFields, LastPaymentMethod, InvitedEmailsToAccountIDs, From b84b0bf483e2db39fa1d290182ffdb06285fd9a7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 15 Feb 2024 04:47:04 +0500 Subject: [PATCH 002/189] fix: type errors --- src/types/onyx/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fbd61a9c5365..4a8b41ca4c5b 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type * as OnyxCommon from './OnyxCommon'; import type PersonalDetails from './PersonalDetails'; -import type {PolicyReportField} from './PolicyReportField'; +import type {PolicyReportField} from './Policy'; type NotificationPreference = ValueOf; From 9054944828891069752adb31d42745713f29dba6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 18:31:12 +0530 Subject: [PATCH 003/189] Initial implementation of button --- assets/images/track-expense.svg | 1 + src/CONST.ts | 1 + src/components/Icon/Expensicons.ts | 2 ++ src/libs/Permissions.ts | 5 ++++ .../FloatingActionButtonAndPopover.js | 23 +++++++++++++------ 5 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 assets/images/track-expense.svg diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg new file mode 100644 index 000000000000..6fb7eb9befec --- /dev/null +++ b/assets/images/track-expense.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index 8abd4c087b16..a26bd61f3b1e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -307,6 +307,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + TRACK_EXPENSE: 'trackExpense', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 2a7ed30abf1a..5bdec7ca7174 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -135,6 +135,7 @@ import Sync from '@assets/images/sync.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; +import TrackExpense from '@assets/images/track-expense.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -302,4 +303,5 @@ export { ChatBubbleAdd, ChatBubbleUnread, Lightbulb, + TrackExpense, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index ce5e0e674c59..52276783576d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } +function canUseTrackExpense(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -39,5 +43,6 @@ export default { canUseCommentLinking, canUseLinkPreviews, canUseViolations, + canUseTrackExpense, canUseReportFields, }; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 573cbe370aa7..85c5ddd55dd8 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -9,6 +9,7 @@ import withNavigation from '@components/withNavigation'; import withNavigationFocus from '@components/withNavigationFocus'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -75,6 +76,7 @@ function FloatingActionButtonAndPopover(props) { const {translate} = useLocalize(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); + const {canUseTrackExpense} = usePermissions(); const prevIsFocused = usePrevious(props.isFocused); @@ -179,13 +181,20 @@ function FloatingActionButtonAndPopover(props) { text: translate('iou.sendMoney'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), }, - ...[ - { - icon: Expensicons.Task, - text: translate('newTaskPage.assignTask'), - onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), - }, - ], + ...(canUseTrackExpense + ? [ + { + icon: Expensicons.TrackExpense, + text: 'Track Expense', + onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + }, + ] + : []), + { + icon: Expensicons.Task, + text: translate('newTaskPage.assignTask'), + onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), + }, { icon: Expensicons.Heart, text: translate('sidebarScreen.saveTheWorld'), From c532babaa7b0fcbc8845cbf7c1578705c4f34453 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 19:17:03 +0530 Subject: [PATCH 004/189] Fixed svg --- assets/images/track-expense.svg | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg index 6fb7eb9befec..c15f28b72dd7 100644 --- a/assets/images/track-expense.svg +++ b/assets/images/track-expense.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + + + + \ No newline at end of file From 2feddbf24d392f85e37c3999b9f5e22f75f518e5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 21:19:04 +0530 Subject: [PATCH 005/189] Trying to start track expense flow --- src/CONST.ts | 2 ++ src/libs/IOUUtils.ts | 2 +- src/libs/ReportUtils.ts | 8 ++++++++ .../FloatingActionButtonAndPopover.js | 20 ++++++++++++++++++- src/pages/iou/request/IOURequestStartPage.js | 3 ++- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index a26bd61f3b1e..9b0afa627672 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -690,6 +690,7 @@ const CONST = { DOMAIN_ALL: 'domainAll', POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', + SELF_DM: 'selfDM', }, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', @@ -1264,6 +1265,7 @@ const CONST = { SEND: 'send', SPLIT: 'split', REQUEST: 'request', + TRACK_EXPENSE: 'track-expense', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 56ac47676a37..07bb22f43b31 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -104,7 +104,7 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { * Checks if the iou type is one of request, send, or split. */ function isValidMoneyRequestType(iouType: string): boolean { - const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND]; + const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE]; return moneyRequestType.includes(iouType); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 747ba27780a3..e634689041d5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4248,6 +4248,10 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return !isPolicyExpenseChat(report) || isOwnPolicyExpenseChat; } +function isSelfDM(report: OnyxEntry): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM; +} + /** * Helper method to define what money request options we want to show for particular method. * There are 3 money request options: Request, Split and Send: @@ -4284,6 +4288,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 1; let options: Array> = []; + if (isSelfDM(report)) { + options = [CONST.IOU.TYPE.TRACK_EXPENSE]; + } + // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option // unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 2 other people in the chat. diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 85c5ddd55dd8..bcf9c77ac2f7 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -51,6 +51,12 @@ const propTypes = { name: PropTypes.string, }), + /** The account details for the logged in user */ + account: PropTypes.shape({ + /** Whether or not the user is a policy admin */ + selfDMReportID: PropTypes.string, + }), + /** Indicated whether the report data is loading */ isLoading: PropTypes.bool, @@ -63,6 +69,7 @@ const defaultProps = { allPolicies: {}, isLoading: false, innerRef: null, + account: {}, }; /** @@ -186,7 +193,15 @@ function FloatingActionButtonAndPopover(props) { { icon: Expensicons.TrackExpense, text: 'Track Expense', - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest_temporaryForRefactor( + CONST.IOU.TYPE.TRACK_EXPENSE, + // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. + // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. + props.account.selfDMReportID || ReportUtils.generateReportID(), + ), + ), }, ] : []), @@ -255,5 +270,8 @@ export default compose( isLoading: { key: ONYXKEYS.IS_LOADING_APP, }, + account: { + key: ONYXKEYS.ACCOUNT, + }, }), )(FloatingActionButtonAndPopoverWithRef); diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 05e3d7c96311..8b7ef2a17973 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -75,6 +75,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), + [CONST.IOU.TYPE.TRACK_EXPENSE]: 'Track Expense', }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); @@ -103,7 +104,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK_EXPENSE; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7da97c34cc2b..7de121af52b4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -122,7 +122,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate) { + if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index b23420b5ef69..d6b90c0de439 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -182,7 +182,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate) { + if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } From f2741741f2ea556ab643f07dbd3e3a8fac2dd853 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 22:09:35 +0530 Subject: [PATCH 006/189] Minor fixes --- ...eyTemporaryForRefactorRequestConfirmationList.js | 7 +++++-- .../iou/request/step/IOURequestStepConfirmation.js | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8eeaeaf87eff..894525ca7a02 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -247,6 +247,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const {unit, rate, currency} = mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); @@ -370,7 +371,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; - if (isTypeSplit && iouAmount === 0) { + if (isTypeTrackExpense) { + text = "Track Expense"; + } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { text = translate('iou.request'); @@ -387,7 +390,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeSplit, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 0744fbd600a7..01b0c74d6f64 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -95,7 +95,16 @@ function IOURequestStepConfirmation({ const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); - const headerTitle = iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.split') : translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + const headerTitle = useMemo(() => { + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.splitBill'); + } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return 'Track Expense'; + } + return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + } + , [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { @@ -407,7 +416,7 @@ function IOURequestStepConfirmation({ Date: Wed, 28 Feb 2024 09:25:09 +0530 Subject: [PATCH 007/189] Temporary disable track expense distance --- src/pages/iou/request/IOURequestStartPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 8b7ef2a17973..b0d2983c55be 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -104,7 +104,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK_EXPENSE; + const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); From d13e2bed6170dd4f22c6f139c1391f18d3877926 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 29 Feb 2024 06:17:39 +0530 Subject: [PATCH 008/189] fix: IOU - CMD+ENTER command takes you to the IOU confirmation page without selecting members. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 21 ++++++-- src/components/SelectionList/types.ts | 2 +- src/pages/RoomInvitePage.tsx | 52 ++++++++++++------- src/pages/workspace/WorkspaceInvitePage.tsx | 26 +++++++--- 4 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1c69d00b3910..16424518d53d 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -371,11 +371,22 @@ function BaseSelectionList( }); /** Calls confirm action when pressing CTRL (CMD) + Enter */ - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm ?? selectFocusedOption, { - captureOnInputs: true, - shouldBubble: !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && isFocused, - }); + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, + (e) => { + const focusedOption = flattenedSections.allOptions[focusedIndex]; + if (onConfirm && (flattenedSections.selectedOptions.length || focusedOption)) { + onConfirm(e, focusedOption); + return; + } + selectFocusedOption(); + }, + { + captureOnInputs: true, + shouldBubble: !flattenedSections.allOptions[focusedIndex], + isActive: !disableKeyboardShortcuts && isFocused, + }, + ); return ( = Partial & { confirmButtonText?: string; /** Callback to fire when the confirm button is pressed */ - onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void; + onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: TItem) => void; /** Whether to show the vertical scroll indicator */ showScrollIndicator?: boolean; diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 40a1b009b38d..66d13d496e8c 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -2,7 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {SectionListData} from 'react-native'; +import type {GestureResponderEvent, SectionListData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -164,31 +164,47 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa [selectedOptions], ); - const validate = useCallback(() => selectedOptions.length > 0, [selectedOptions]); + const validate = useCallback((options: ReportUtils.OptionData[]) => options.length > 0, []); // Non policy members should not be able to view the participants of a room const reportID = report?.reportID; const isPolicyMember = useMemo(() => (report?.policyID ? PolicyUtils.isPolicyMember(report.policyID, policies as Record) : false), [report?.policyID, policies]); const backRoute = useMemo(() => reportID && (isPolicyMember ? ROUTES.ROOM_MEMBERS.getRoute(reportID) : ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)), [isPolicyMember, reportID]); const reportName = useMemo(() => ReportUtils.getReportName(report), [report]); - const inviteUsers = useCallback(() => { - if (!validate()) { - return; - } - const invitedEmailsToAccountIDs: PolicyUtils.MemberEmailsToAccountIDs = {}; - selectedOptions.forEach((option) => { - const login = option.login ?? ''; - const accountID = option.accountID; - if (!login.toLowerCase().trim() || !accountID) { + + const inviteUsers = useCallback( + (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { + const options = [...selectedOptions]; + + if (option && e && 'key' in e && e.key === 'Enter') { + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); + + if (option && !isOptionInList) { + toggleOption(option); + options.push(option); + } + } + + if (!validate(options)) { return; } - invitedEmailsToAccountIDs[login] = Number(accountID); - }); - if (reportID) { - Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); - } - Navigation.navigate(backRoute); - }, [selectedOptions, backRoute, reportID, validate]); + + const invitedEmailsToAccountIDs: PolicyUtils.MemberEmailsToAccountIDs = {}; + options.forEach((selectedOption) => { + const login = selectedOption.login ?? ''; + const accountID = selectedOption.accountID; + if (!login.toLowerCase().trim() || !accountID) { + return; + } + invitedEmailsToAccountIDs[login] = Number(accountID); + }); + if (reportID) { + Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); + } + Navigation.navigate(backRoute); + }, + [selectedOptions, backRoute, reportID, validate, toggleOption], + ); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3c1b009aac70..e59176b6d79c 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -2,7 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useEffect, useMemo, useState} from 'react'; -import type {SectionListData} from 'react-native'; +import type {GestureResponderEvent, SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -236,9 +236,9 @@ function WorkspaceInvitePage({ setSelectedOptions(newSelectedOptions); }; - const validate = (): boolean => { + const validate = (options: OptionsListUtils.MemberForList[]): boolean => { const errors: Errors = {}; - if (selectedOptions.length <= 0) { + if (options.length <= 0) { errors.noUserSelected = 'true'; } @@ -246,15 +246,25 @@ function WorkspaceInvitePage({ return isEmptyObject(errors); }; - const inviteUser = () => { - if (!validate()) { + const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { + const options = [...selectedOptions]; + if (option && e && 'key' in e && e.key === 'Enter') { + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); + + if (option && !isOptionInList) { + toggleOption(option); + options.push(option); + } + } + + if (!validate(options)) { return; } const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; - selectedOptions.forEach((option) => { - const login = option.login ?? ''; - const accountID = option.accountID ?? ''; + options.forEach((selectedOption) => { + const login = selectedOption.login ?? ''; + const accountID = selectedOption.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } From 06ca091a5fef7552178ee4d767f7bf709a87abf4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 29 Feb 2024 09:49:30 +0530 Subject: [PATCH 009/189] fix: prevent tab switch while swiping horizontally on map --- src/components/SwipeInterceptPanResponder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx index fe1545d2f14b..48cfe4f90c5c 100644 --- a/src/components/SwipeInterceptPanResponder.tsx +++ b/src/components/SwipeInterceptPanResponder.tsx @@ -1,7 +1,7 @@ import {PanResponder} from 'react-native'; const SwipeInterceptPanResponder = PanResponder.create({ - onMoveShouldSetPanResponder: () => true, + onStartShouldSetPanResponder: () => true, onPanResponderTerminationRequest: () => false, }); From 4607adda4ca244422ffce548780750df2d01610a Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 29 Feb 2024 10:13:51 +0530 Subject: [PATCH 010/189] misc: remove redundant file --- src/components/MapView/responder/index.android.ts | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/components/MapView/responder/index.android.ts diff --git a/src/components/MapView/responder/index.android.ts b/src/components/MapView/responder/index.android.ts deleted file mode 100644 index a0fce71d8ef5..000000000000 --- a/src/components/MapView/responder/index.android.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {PanResponder} from 'react-native'; - -const responder = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onPanResponderTerminationRequest: () => false, -}); - -export default responder; From f9dd242735e6e64019807b4c5c981b0c80c89add Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 29 Feb 2024 10:51:11 +0100 Subject: [PATCH 011/189] migrate group 5 tests to ts --- .../actions/{ReportTest.js => ReportTest.ts} | 135 +++++++++--------- tests/unit/{APITest.js => APITest.ts} | 93 ++++++++---- .../{MigrationTest.js => MigrationTest.ts} | 61 ++++---- tests/unit/{NetworkTest.js => NetworkTest.ts} | 32 +++-- 4 files changed, 193 insertions(+), 128 deletions(-) rename tests/actions/{ReportTest.js => ReportTest.ts} (86%) rename tests/unit/{APITest.js => APITest.ts} (87%) rename tests/unit/{MigrationTest.js => MigrationTest.ts} (76%) rename tests/unit/{NetworkTest.js => NetworkTest.ts} (92%) diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.ts similarity index 86% rename from tests/actions/ReportTest.js rename to tests/actions/ReportTest.ts index a94db507637b..43ceaaad607e 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.ts @@ -1,8 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/globals'; import {utcToZonedTime} from 'date-fns-tz'; -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; import CONST from '../../src/CONST'; import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; @@ -21,7 +22,7 @@ import waitForNetworkPromises from '../utils/waitForNetworkPromises'; const UTC = 'UTC'; jest.mock('../../src/libs/actions/Report', () => { - const originalModule = jest.requireActual('../../src/libs/actions/Report'); + const originalModule: typeof Report = jest.requireActual('../../src/libs/actions/Report'); return { ...originalModule, @@ -35,7 +36,7 @@ describe('actions/Report', () => { PusherHelper.setup(); Onyx.init({ keys: ONYXKEYS, - registerStorageEventListener: () => {}, + // registerStorageEventListener: () => {}, }); }); @@ -52,12 +53,12 @@ describe('actions/Report', () => { afterEach(PusherHelper.teardown); it('should store a new report action in Onyx when onyxApiUpdate event is handled via Pusher', () => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; - const REPORT_ID = 1; - let reportActionID; + const REPORT_ID = '1'; + let reportActionID: string; const REPORT_ACTION = { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, actorAccountID: TEST_USER_ACCOUNT_ID, @@ -68,7 +69,7 @@ describe('actions/Report', () => { shouldShow: true, }; - let reportActions; + let reportActions: OnyxEntry; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val), @@ -88,7 +89,7 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - const resultAction = _.first(_.values(reportActions)); + const resultAction: OnyxEntry = Object.values(reportActions ?? [])[0]; reportActionID = resultAction.reportActionID; expect(resultAction.message).toEqual(REPORT_ACTION.message); @@ -125,12 +126,12 @@ describe('actions/Report', () => { }) .then(() => { // Verify there is only one action and our optimistic comment has been removed - expect(_.size(reportActions)).toBe(1); + expect(Object.keys(reportActions ?? {}).length).toBe(1); - const resultAction = reportActions[reportActionID]; + const resultAction = reportActions?.[reportActionID]; // Verify that our action is no longer in the loading state - expect(resultAction.pendingAction).toBeUndefined(); + expect(resultAction?.pendingAction).toBeUndefined(); }); }); @@ -139,10 +140,10 @@ describe('actions/Report', () => { const TEST_USER_LOGIN = 'test@test.com'; const REPORT_ID = '1'; - let reportIsPinned; + let reportIsPinned: boolean; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, - callback: (val) => (reportIsPinned = lodashGet(val, 'isPinned')), + callback: (val) => (reportIsPinned = val?.isPinned ?? false), }); // Set up Onyx with some test user data @@ -167,7 +168,7 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) .then(() => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; // WHEN we add enough logs to send a packet for (let i = 0; i <= LOGGER_MAX_LOG_LINES; i++) { @@ -186,27 +187,27 @@ describe('actions/Report', () => { .then(() => { // THEN only ONE call to AddComment will happen const URL_ARGUMENT_INDEX = 0; - const addCommentCalls = _.filter(global.fetch.mock.calls, (callArguments) => callArguments[URL_ARGUMENT_INDEX].includes('AddComment')); + const addCommentCalls = (global.fetch as jest.Mock).mock.calls.filter((callArguments) => callArguments[URL_ARGUMENT_INDEX].includes('AddComment')); expect(addCommentCalls.length).toBe(1); }); }); it('should be updated correctly when new comments are added, deleted or marked as unread', () => { jest.useFakeTimers(); - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const REPORT_ID = '1'; - let report; - let reportActionCreatedDate; - let currentTime; + let report: OnyxEntry; + let reportActionCreatedDate: string; + let currentTime: string; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, callback: (val) => (report = val), }); - let reportActions; + let reportActions: OnyxTypes.ReportActions; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: (val) => (reportActions = val), + callback: (val) => (reportActions = val ?? {}), }); const USER_1_LOGIN = 'user@test.com'; @@ -276,7 +277,7 @@ describe('actions/Report', () => { .then(() => { // The report will be read expect(ReportUtils.isUnread(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); // And no longer show the green dot for unread mentions in the LHN expect(ReportUtils.isUnreadWithMention(report)).toBe(false); @@ -290,7 +291,7 @@ describe('actions/Report', () => { // Then the report will be unread and show the green dot for unread mentions in LHN expect(ReportUtils.isUnread(report)).toBe(true); expect(ReportUtils.isUnreadWithMention(report)).toBe(true); - expect(report.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); + expect(report?.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); // When a new comment is added by the current user jest.advanceTimersByTime(10); @@ -302,8 +303,8 @@ describe('actions/Report', () => { // The report will be read, the green dot for unread mentions will go away, and the lastReadTime updated expect(ReportUtils.isUnread(report)).toBe(false); expect(ReportUtils.isUnreadWithMention(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); - expect(report.lastMessageText).toBe('Current User Comment 1'); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(report?.lastMessageText).toBe('Current User Comment 1'); // When another comment is added by the current user jest.advanceTimersByTime(10); @@ -314,8 +315,8 @@ describe('actions/Report', () => { .then(() => { // The report will be read and the lastReadTime updated expect(ReportUtils.isUnread(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); - expect(report.lastMessageText).toBe('Current User Comment 2'); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(report?.lastMessageText).toBe('Current User Comment 2'); // When another comment is added by the current user jest.advanceTimersByTime(10); @@ -326,8 +327,8 @@ describe('actions/Report', () => { .then(() => { // The report will be read and the lastReadTime updated expect(ReportUtils.isUnread(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); - expect(report.lastMessageText).toBe('Current User Comment 3'); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(report?.lastMessageText).toBe('Current User Comment 3'); const USER_1_BASE_ACTION = { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, @@ -350,12 +351,14 @@ describe('actions/Report', () => { created: DateUtils.getDBTime(Date.now() - 2), reportActionID: '200', }, + 300: { ...USER_1_BASE_ACTION, message: [{type: 'COMMENT', html: 'Current User Comment 2', text: 'Current User Comment 2'}], created: DateUtils.getDBTime(Date.now() - 1), reportActionID: '300', }, + 400: { ...USER_1_BASE_ACTION, message: [{type: 'COMMENT', html: 'Current User Comment 3', text: 'Current User Comment 3'}], @@ -394,7 +397,7 @@ describe('actions/Report', () => { }) .then(() => { // Then no change will occur - expect(report.lastReadTime).toBe(reportActionCreatedDate); + expect(report?.lastReadTime).toBe(reportActionCreatedDate); expect(ReportUtils.isUnread(report)).toBe(false); // When the user manually marks a message as "unread" @@ -404,7 +407,7 @@ describe('actions/Report', () => { .then(() => { // Then we should expect the report to be to be unread expect(ReportUtils.isUnread(report)).toBe(true); - expect(report.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); + expect(report?.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); // If the user deletes the last comment after the lastReadTime the lastMessageText will reflect the new last comment Report.deleteReportComment(REPORT_ID, {...reportActions[400]}); @@ -412,7 +415,7 @@ describe('actions/Report', () => { }) .then(() => { expect(ReportUtils.isUnread(report)).toBe(false); - expect(report.lastMessageText).toBe('Current User Comment 2'); + expect(report?.lastMessageText).toBe('Current User Comment 2'); }); waitForBatchedUpdates(); // flushing onyx.set as it will be batched return setPromise; @@ -424,7 +427,7 @@ describe('actions/Report', () => { * already in the comment and the user deleted it on purpose. */ - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; // User edits comment to add link // We should generate link @@ -536,11 +539,11 @@ describe('actions/Report', () => { }); it('should properly toggle reactions on a message', () => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; - const REPORT_ID = 1; + const REPORT_ID = '1'; const EMOJI_CODE = '👍'; const EMOJI_SKIN_TONE = 2; const EMOJI_NAME = '+1'; @@ -550,20 +553,20 @@ describe('actions/Report', () => { types: ['👍🏿', '👍🏾', '👍🏽', '👍🏼', '👍🏻'], }; - let reportActions; + let reportActions: OnyxTypes.ReportActions; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: (val) => (reportActions = val), + callback: (val) => (reportActions = val ?? {}), }); - const reportActionsReactions = {}; + const reportActionsReactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS, callback: (val, key) => { - reportActionsReactions[key] = val; + reportActionsReactions[key] = val ?? {}; }, }); - let reportAction; - let reportActionID; + let reportAction: OnyxTypes.ReportAction; + let reportActionID: string; // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -579,15 +582,15 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; reportActionID = reportAction.reportActionID; // Add a reaction to the comment - Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI); + Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionsReactions[0]); return waitForBatchedUpdates(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; // Expect the reaction to exist in the reportActionsReactions collection expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); @@ -597,8 +600,8 @@ describe('actions/Report', () => { expect(reportActionReaction).toHaveProperty(EMOJI.name); // Expect the emoji to have the user accountID - const reportActionReactionEmoji = reportActionReaction[EMOJI.name]; - expect(reportActionReactionEmoji.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); + const reportActionReactionEmoji = reportActionReaction?.[EMOJI.name]; + expect(reportActionReactionEmoji?.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); // Now we remove the reaction Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionReaction); @@ -608,23 +611,23 @@ describe('actions/Report', () => { // Expect the reaction to have null where the users reaction used to be expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`]; - expect(reportActionReaction[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); + expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; // Add the same reaction to the same report action with a different skintone - Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI); + Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionsReactions[0]); return waitForBatchedUpdates() .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`]; Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionReaction, EMOJI_SKIN_TONE); return waitForBatchedUpdates(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; // Expect the reaction to exist in the reportActionsReactions collection expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); @@ -634,11 +637,11 @@ describe('actions/Report', () => { expect(reportActionReaction).toHaveProperty(EMOJI.name); // Expect the emoji to have the user accountID - const reportActionReactionEmoji = reportActionReaction[EMOJI.name]; - expect(reportActionReactionEmoji.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); + const reportActionReactionEmoji = reportActionReaction?.[EMOJI.name]; + expect(reportActionReactionEmoji?.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); // Expect two different skintone reactions - const reportActionReactionEmojiUserSkinTones = reportActionReactionEmoji.users[TEST_USER_ACCOUNT_ID].skinTones; + const reportActionReactionEmojiUserSkinTones = reportActionReactionEmoji?.users[TEST_USER_ACCOUNT_ID].skinTones; expect(reportActionReactionEmojiUserSkinTones).toHaveProperty('-1'); expect(reportActionReactionEmojiUserSkinTones).toHaveProperty('2'); @@ -650,17 +653,17 @@ describe('actions/Report', () => { // Expect the reaction to have null where the users reaction used to be expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`]; - expect(reportActionReaction[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); + expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }); }); }); it("shouldn't add the same reaction twice when changing preferred skin color and reaction doesn't support skin colors", () => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; - const REPORT_ID = 1; + const REPORT_ID = '1'; const EMOJI_CODE = '😄'; const EMOJI_NAME = 'smile'; const EMOJI = { @@ -668,20 +671,20 @@ describe('actions/Report', () => { name: EMOJI_NAME, }; - let reportActions; + let reportActions: OnyxTypes.ReportActions = {}; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: (val) => (reportActions = val), + callback: (val) => (reportActions = val ?? {}), }); - const reportActionsReactions = {}; + const reportActionsReactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS, callback: (val, key) => { - reportActionsReactions[key] = val; + reportActionsReactions[key] = val ?? {}; }, }); - let resultAction; + let resultAction: OnyxTypes.ReportAction; // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -697,14 +700,14 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - resultAction = _.first(_.values(reportActions)); + resultAction = Object.values(reportActions)[0]; // Add a reaction to the comment Report.toggleEmojiReaction(REPORT_ID, resultAction, EMOJI, {}); return waitForBatchedUpdates(); }) .then(() => { - resultAction = _.first(_.values(reportActions)); + resultAction = Object.values(reportActions)[0]; // Now we toggle the reaction while the skin tone has changed. // As the emoji doesn't support skin tones, the emoji @@ -717,7 +720,7 @@ describe('actions/Report', () => { // Expect the reaction to have null where the users reaction used to be expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${resultAction.reportActionID}`); const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${resultAction.reportActionID}`]; - expect(reportActionReaction[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); + expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }); }); }); diff --git a/tests/unit/APITest.js b/tests/unit/APITest.ts similarity index 87% rename from tests/unit/APITest.js rename to tests/unit/APITest.ts index 30c935c48571..9c94730fb4cc 100644 --- a/tests/unit/APITest.js +++ b/tests/unit/APITest.ts @@ -1,5 +1,6 @@ -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +// import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import CONST from '../../src/CONST'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; import * as API from '../../src/libs/API'; @@ -14,16 +15,26 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; +const Onyx = reactNativeOnyxMock; + jest.mock('../../src/libs/Log'); Onyx.init({ keys: ONYXKEYS, }); +type Response = { + ok?: boolean; + status?: ValueOf | ValueOf; + jsonCode?: ValueOf; + title?: ValueOf; + type?: ValueOf; +}; + const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); @@ -53,8 +64,11 @@ describe('APITests', () => { return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Writes and Reads are called + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); + // @ts-expect-error - mocking the parameter API.read('mock command', {param2: 'value2'}); + // @ts-expect-error - mocking the parameter API.write('mock command', {param3: 'value3'}); return waitForBatchedUpdates(); }) @@ -89,7 +103,9 @@ describe('APITests', () => { }) .then(() => { // When API Write commands are made + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('mock command', {param2: 'value2'}); return waitForBatchedUpdates(); }) @@ -120,8 +136,11 @@ describe('APITests', () => { test('Write request should not be cleared until a backend response occurs', () => { // We're setting up xhr handler that will resolve calls programmatically - const xhrCalls = []; - const promises = []; + const xhrCalls: Array<{ + resolve: (value: Response | PromiseLike) => void; + reject: (value: unknown) => void; + }> = []; + const promises: Array> = []; jest.spyOn(HttpUtils, 'xhr').mockImplementation(() => { promises.push( @@ -130,7 +149,7 @@ describe('APITests', () => { }), ); - return _.last(promises); + return promises.slice(-1)[0]; }); // Given we have some requests made while we're offline @@ -138,7 +157,9 @@ describe('APITests', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Write commands are made + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('mock command', {param2: 'value2'}); return waitForBatchedUpdates(); }) @@ -148,14 +169,14 @@ describe('APITests', () => { .then(waitForBatchedUpdates) .then(() => { // Then requests should remain persisted until the xhr call is resolved - expect(_.size(PersistedRequests.getAll())).toEqual(2); + expect(PersistedRequests.getAll().length).toEqual(2); xhrCalls[0].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); return waitForBatchedUpdates(); }) .then(waitForBatchedUpdates) .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); // When a request fails it should be retried @@ -163,7 +184,7 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); // We need to advance past the request throttle back off timer because the request won't be retried until then @@ -177,32 +198,30 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(0); + expect(PersistedRequests.getAll().length).toEqual(0); }) ); }); // Given a retry response create a mock and run some expectations for retrying requests - const retryExpectations = (retryResponse) => { - let successfulResponse = { + + const retryExpectations = (Response: Response) => { + const successfulResponse = { ok: true, jsonCode: CONST.JSON_CODE.SUCCESS, - }; - - // We have to mock response.json() too - successfulResponse = { - ...successfulResponse, + // We have to mock response.json() too json: () => Promise.resolve(successfulResponse), }; // Given a mock where a retry response is returned twice before a successful response - global.fetch = jest.fn().mockResolvedValueOnce(retryResponse).mockResolvedValueOnce(retryResponse).mockResolvedValueOnce(successfulResponse); + global.fetch = jest.fn().mockResolvedValueOnce(Response).mockResolvedValueOnce(Response).mockResolvedValueOnce(successfulResponse); // Given we have a request made while we're offline return ( Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Write commands are made + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); return waitForNetworkPromises(); }) @@ -215,7 +234,7 @@ describe('APITests', () => { expect(global.fetch).toHaveBeenCalledTimes(1); // And we still have 1 persisted request since it failed - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})})]); // We let the SequentialQueue process again after its wait time @@ -228,7 +247,7 @@ describe('APITests', () => { expect(global.fetch).toHaveBeenCalledTimes(2); // And we still have 1 persisted request since it failed - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})})]); // We let the SequentialQueue process again after its wait time @@ -241,7 +260,7 @@ describe('APITests', () => { expect(global.fetch).toHaveBeenCalledTimes(3); // The request succeeds so the queue is empty - expect(_.size(PersistedRequests.getAll())).toEqual(0); + expect(PersistedRequests.getAll().length).toEqual(0); }) ); }; @@ -258,7 +277,7 @@ describe('APITests', () => { // Given the response data returned when auth is down const responseData = { ok: true, - status: 200, + status: CONST.JSON_CODE.SUCCESS, jsonCode: CONST.JSON_CODE.EXP_ERROR, title: CONST.ERROR_TITLE.SOCKET, type: CONST.ERROR_TYPE.SOCKET, @@ -289,6 +308,7 @@ describe('APITests', () => { waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) .then(() => { + // @ts-expect-error - mocking the parameter API.write('Mock', {param1: 'value1'}); return waitForBatchedUpdates(); }) @@ -297,7 +317,7 @@ describe('APITests', () => { .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) .then(waitForBatchedUpdates) .then(() => { - const nonLogCalls = _.filter(xhr.mock.calls, ([commandName]) => commandName !== 'Log'); + const nonLogCalls = xhr.mock.calls.filter(([commandName]) => commandName !== 'Log'); // The request should be retried once and reauthenticate should be called the second time // expect(xhr).toHaveBeenCalledTimes(3); @@ -322,12 +342,19 @@ describe('APITests', () => { }) .then(() => { // When we queue 6 persistable commands and one not persistable + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value2'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value3'}); + // @ts-expect-error - mocking the parameter API.read('MockCommand', {content: 'not-persisted'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value4'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value5'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value6'}); return waitForBatchedUpdates(); @@ -359,11 +386,17 @@ describe('APITests', () => { }) .then(() => { // When we queue 6 persistable commands + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value2'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value3'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value4'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value5'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value6'}); return waitForBatchedUpdates(); }) @@ -402,7 +435,14 @@ describe('APITests', () => { }) .then(() => { // When we queue both non-persistable and persistable commands that will trigger reauthentication and go offline at the same time - API.makeRequestWithSideEffects('AuthenticatePusher', {content: 'value1'}); + API.makeRequestWithSideEffects('AuthenticatePusher', { + // eslint-disable-next-line @typescript-eslint/naming-convention + socket_id: 'socket_id', + // eslint-disable-next-line @typescript-eslint/naming-convention + channel_name: 'channel_name', + shouldRetry: false, + forceNetworkRequest: false, + }); Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); expect(NetworkStore.isOffline()).toBe(false); @@ -410,6 +450,7 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { + // @ts-expect-error - mocking the parameter API.write('MockCommand'); expect(PersistedRequests.getAll().length).toBe(1); expect(NetworkStore.isOffline()).toBe(true); @@ -479,6 +520,7 @@ describe('APITests', () => { NetworkStore.resetHasReadRequiredDataFromStorage(); // And queue a Write request while offline + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value1'}); // Then we should expect the request to get persisted @@ -515,8 +557,11 @@ describe('APITests', () => { expect(NetworkStore.isOffline()).toBe(false); // WHEN we make a request that should be retried, one that should not, and another that should + // @ts-expect-error - mocking the parameter API.write('MockCommandOne'); + // @ts-expect-error - mocking the parameter API.read('MockCommandTwo'); + // @ts-expect-error - mocking the parameter API.write('MockCommandThree'); // THEN the retryable requests should immediately be added to the persisted requests diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.ts similarity index 76% rename from tests/unit/MigrationTest.js rename to tests/unit/MigrationTest.ts index 65ab921ac9e1..6d18ec2f0c68 100644 --- a/tests/unit/MigrationTest.js +++ b/tests/unit/MigrationTest.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; import Log from '../../src/libs/Log'; import CheckForPreviousReportActionID from '../../src/libs/migrations/CheckForPreviousReportActionID'; @@ -7,13 +8,13 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('../../src/libs/getPlatform'); -let LogSpy; +let LogSpy: unknown; describe('Migrations', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); LogSpy = jest.spyOn(Log, 'info'); - Log.serverLoggingCallback = () => {}; + Log.serverLoggingCallback = () => Promise.resolve({requestID: '123'}); return waitForBatchedUpdates(); }); @@ -32,6 +33,7 @@ describe('Migrations', () => { it('Should remove all report actions given that a previousReportActionID does not exist', () => Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { + // @ts-expect-error Preset necessary values 1: { reportActionID: '1', }, @@ -51,7 +53,7 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportAction = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); })); @@ -59,6 +61,7 @@ describe('Migrations', () => { it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { + // @ts-expect-error Preset necessary values 1: { reportActionID: '1', previousReportActionID: '0', @@ -87,12 +90,13 @@ describe('Migrations', () => { previousReportActionID: '1', }, }; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); })); it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, @@ -117,15 +121,16 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportAction = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); }, }); })); it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, @@ -160,15 +165,16 @@ describe('Migrations', () => { previousReportActionID: '23', }, }; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction1); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction1); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); }, }); })); it('Should skip if no valid reportActions', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, @@ -184,10 +190,10 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportAction = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); }, }); })); @@ -200,6 +206,7 @@ describe('Migrations', () => { )); it('Should move individual draft to a draft collection of report', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: 'a', [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: 'b', @@ -221,16 +228,17 @@ describe('Migrations', () => { 3: 'c', 4: 'd', }; - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]).toMatchObject(expectedReportActionDraft1); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft2); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]).toMatchObject(expectedReportActionDraft1); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft2); }, }); })); it('Should skip if nothing to migrate', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: null, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: null, @@ -246,15 +254,16 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportActionDraft = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft); }, }); })); it("Shouldn't move empty individual draft to a draft collection of report", () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: '', [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]: {}, @@ -266,7 +275,7 @@ describe('Migrations', () => { waitForCollectionCallback: true, callback: (allReportActionsDrafts) => { Onyx.disconnect(connectionID); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); }, }); })); diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.ts similarity index 92% rename from tests/unit/NetworkTest.js rename to tests/unit/NetworkTest.ts index 29f5e344b35a..f8b5b6a7d345 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.ts @@ -1,5 +1,6 @@ -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import type {Mock} from 'jest-mock'; +import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; +// import Onyx from 'react-native-onyx'; import CONST from '../../src/CONST'; import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; @@ -15,6 +16,8 @@ import ONYXKEYS from '../../src/ONYXKEYS'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +const Onyx = reactNativeOnyxMock; + jest.mock('../../src/libs/Log'); Onyx.init({ @@ -25,7 +28,7 @@ OnyxUpdateManager(); const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); @@ -50,7 +53,7 @@ describe('NetworkTests', () => { const TEST_USER_LOGIN = 'test@testguy.com'; const TEST_USER_ACCOUNT_ID = 1; - let isOffline; + let isOffline: boolean | null = null; Onyx.connect({ key: ONYXKEYS.NETWORK, @@ -67,8 +70,9 @@ describe('NetworkTests', () => { global.fetch = jest.fn().mockRejectedValue(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); const actualXhr = HttpUtils.xhr; - HttpUtils.xhr = jest.fn(); - HttpUtils.xhr + + const mockedXhr = jest.fn(); + mockedXhr .mockImplementationOnce(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, @@ -100,6 +104,8 @@ describe('NetworkTests', () => { }), ); + HttpUtils.xhr = mockedXhr; + // This should first trigger re-authentication and then a Failed to fetch PersonalDetails.openPersonalDetails(); return waitForBatchedUpdates() @@ -113,8 +119,8 @@ describe('NetworkTests', () => { }) .then(() => { // Then we will eventually have 1 call to OpenPersonalDetailsPage and 1 calls to Authenticate - const callsToOpenPersonalDetails = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'OpenPersonalDetailsPage'); - const callsToAuthenticate = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'Authenticate'); + const callsToOpenPersonalDetails = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPersonalDetailsPage'); + const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); expect(callsToOpenPersonalDetails.length).toBe(1); expect(callsToAuthenticate.length).toBe(1); @@ -133,8 +139,8 @@ describe('NetworkTests', () => { // When we sign in return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - HttpUtils.xhr = jest.fn(); - HttpUtils.xhr + const mockedXhr = jest.fn(); + mockedXhr // And mock the first call to openPersonalDetails return with an expired session code .mockImplementationOnce(() => @@ -164,6 +170,8 @@ describe('NetworkTests', () => { }), ); + HttpUtils.xhr = mockedXhr; + // And then make 3 API READ requests in quick succession with an expired authToken and handle the response // It doesn't matter which requests these are really as all the response is mocked we just want to see // that we get re-authenticated @@ -175,8 +183,8 @@ describe('NetworkTests', () => { .then(() => { // We should expect to see the three calls to OpenApp, but only one call to Authenticate. // And we should also see the reconnection callbacks triggered. - const callsToOpenPersonalDetails = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'OpenPersonalDetailsPage'); - const callsToAuthenticate = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'Authenticate'); + const callsToOpenPersonalDetails = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPersonalDetailsPage'); + const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); expect(callsToOpenPersonalDetails.length).toBe(3); expect(callsToAuthenticate.length).toBe(1); expect(reconnectionCallbacksSpy.mock.calls.length).toBe(3); From 1512c35d5f251ddd20c26a277319ff7b390ce61e Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:54:35 +0100 Subject: [PATCH 012/189] Fix: Workspace - Member and Role can be clicked to select all the members in Members list --- .../SelectionList/BaseSelectionList.tsx | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1c69d00b3910..843c7ee1fc28 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -429,28 +429,31 @@ function BaseSelectionList( ) : ( <> {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - e.preventDefault() : undefined} - > - + - {customListHeader ?? ( - - {translate('workspace.people.selectAll')} - - )} - + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + > + + {!customListHeader ? ( + + {translate('workspace.people.selectAll')} + + ) : null} + + {customListHeader} + )} {!headerMessage && !canSelectMultiple && customListHeader} Date: Thu, 29 Feb 2024 15:51:05 -0300 Subject: [PATCH 013/189] Migrate NVPs to their new keys --- src/ONYXKEYS.ts | 24 +++++++----- src/libs/migrateOnyx.ts | 3 +- src/libs/migrations/NVPMigration.ts | 61 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/libs/migrations/NVPMigration.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d4a0b8a21d66..d0b73c963ce1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -17,7 +17,7 @@ const ONYXKEYS = { ACCOUNT_MANAGER_REPORT_ID: 'accountManagerReportID', /** Boolean flag only true when first set */ - NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'isFirstTimeNewExpensifyUser', + NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', /** Holds an array of client IDs which is used for multi-tabs on web in order to know * which tab is the leader, and which ones are the followers */ @@ -109,22 +109,25 @@ const ONYXKEYS = { NVP_PRIORITY_MODE: 'nvp_priorityMode', /** Contains the users's block expiration (if they have one) */ - NVP_BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge', + NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge', /** A unique identifier that each user has that's used to send notifications */ - NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'private_pushNotificationID', + NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'nvp_private_pushNotificationID', /** The NVP with the last payment method used per policy */ - NVP_LAST_PAYMENT_METHOD: 'nvp_lastPaymentMethod', + NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod', /** This NVP holds to most recent waypoints that a person has used when creating a distance request */ NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */ - NVP_HAS_DISMISSED_IDLE_PANEL: 'hasDismissedIdlePanel', + NVP_HAS_DISMISSED_IDLE_PANEL: 'nvp_hasDismissedIdlePanel', /** This NVP contains the choice that the user made on the engagement modal */ - NVP_INTRO_SELECTED: 'introSelected', + NVP_INTRO_SELECTED: 'nvp_introSelected', + + /** This NVP contains the active policyID */ + NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID', /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -146,7 +149,7 @@ const ONYXKEYS = { ONFIDO_APPLICANT_ID: 'onfidoApplicantID', /** Indicates which locale should be used */ - NVP_PREFERRED_LOCALE: 'preferredLocale', + NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', /** User's Expensify Wallet */ USER_WALLET: 'userWallet', @@ -170,7 +173,7 @@ const ONYXKEYS = { CARD_LIST: 'cardList', /** Whether the user has tried focus mode yet */ - NVP_TRY_FOCUS_MODE: 'tryFocusMode', + NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', /** Whether the user has been shown the hold educational interstitial yet */ NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', @@ -188,10 +191,10 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', /** Store preferred skintone for emoji */ - PREFERRED_EMOJI_SKIN_TONE: 'preferredEmojiSkinTone', + PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', /** Store frequently used emojis for this user */ - FREQUENTLY_USED_EMOJIS: 'frequentlyUsedEmojis', + FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', @@ -568,6 +571,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LOGS]: Record; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; [ONYXKEYS.CACHED_PDF_PATHS]: Record; + [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 1202275067a5..5ce899cdd316 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -1,5 +1,6 @@ import Log from './Log'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; +import NVPMigration from './migrations/NVPMigration'; import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; @@ -10,7 +11,7 @@ export default function (): Promise { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts, NVPMigration]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts new file mode 100644 index 000000000000..1c3465a492a9 --- /dev/null +++ b/src/libs/migrations/NVPMigration.ts @@ -0,0 +1,61 @@ +import after from 'lodash/after'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const migrations = { + // eslint-disable-next-line @typescript-eslint/naming-convention + nvp_lastPaymentMethod: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + isFirstTimeNewExpensifyUser: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, + preferredLocale: ONYXKEYS.NVP_PREFERRED_LOCALE, + preferredEmojiSkinTone: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + frequentlyUsedEmojis: ONYXKEYS.FREQUENTLY_USED_EMOJIS, + // eslint-disable-next-line @typescript-eslint/naming-convention + private_blockedFromConcierge: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, + // eslint-disable-next-line @typescript-eslint/naming-convention + private_pushNotificationID: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID, + tryFocusMode: ONYXKEYS.NVP_TRY_FOCUS_MODE, + introSelected: ONYXKEYS.NVP_INTRO_SELECTED, + hasDismissedIdlePanel: ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL, +}; + +// This migration changes the keys of all the NVP related keys so that they are standardized +export default function () { + return new Promise((resolve) => { + // It's 1 more because activePolicyID is not in the migrations object above as it is nested inside an object + const resolveWhenDone = after(Object.entries(migrations).length + 1, () => resolve()); + + for (const [oldKey, newKey] of Object.entries(migrations)) { + const connectionID = Onyx.connect({ + // @ts-expect-error oldKey is a variable + key: oldKey, + callback: (value) => { + Onyx.disconnect(connectionID); + if (value !== null) { + // @ts-expect-error These keys are variables, so we can't check the type + Onyx.multiSet({ + [newKey]: value, + [oldKey]: null, + }); + } + resolveWhenDone(); + }, + }); + } + const connectionID = Onyx.connect({ + key: ONYXKEYS.ACCOUNT, + callback: (value) => { + Onyx.disconnect(connectionID); + if (value?.activePolicyID) { + const activePolicyID = value.activePolicyID; + const newValue = value; + delete newValue.activePolicyID; + Onyx.multiSet({ + [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, + [ONYXKEYS.ACCOUNT]: newValue, + }); + } + resolveWhenDone(); + }, + }); + }); +} From c4205502e9c039f5c6a4825052a51b18c1100150 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:39:56 +0100 Subject: [PATCH 014/189] Fix: Category - Checkbox is clickable outside near the right of checkbox --- src/components/SelectionList/BaseListItem.tsx | 2 +- src/styles/utils/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 98b1999625ee..5ea451c12f11 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -79,7 +79,7 @@ function BaseListItem({ accessibilityLabel={item.text} role={CONST.ROLE.BUTTON} onPress={handleCheckboxPress} - style={StyleUtils.getCheckboxPressableStyle()} + style={[StyleUtils.getCheckboxPressableStyle(), styles.mr3]} > {item.isSelected && ( diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 72719e4795c4..5470d976eafe 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1481,7 +1481,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter], getMultiselectListStyles: (isSelected: boolean, isDisabled: boolean): ViewStyle => ({ - ...styles.mr3, ...(isSelected && styles.checkedContainer), ...(isSelected && styles.borderColorFocus), ...(isDisabled && styles.cursorDisabled), From 9ce6a3cf5ff6a901889a83a2e3d4e0a0149f572b Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 29 Feb 2024 17:03:22 -0300 Subject: [PATCH 015/189] Remove nvp props from inside account --- src/ONYXKEYS.ts | 4 ++++ src/components/ReferralProgramCTA.tsx | 5 ++--- src/pages/NewChatPage.tsx | 5 ++--- ...poraryForRefactorRequestParticipantsSelector.js | 3 +-- .../MoneyRequestParticipantsSelector.js | 3 +-- src/pages/workspace/WorkspaceNewRoomPage.tsx | 8 +++----- src/types/onyx/Account.ts | 14 +------------- src/types/onyx/DismissedReferralBanners.ts | 11 +++++++++++ src/types/onyx/index.ts | 2 ++ 9 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 src/types/onyx/DismissedReferralBanners.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d0b73c963ce1..304c091a48a2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -129,6 +129,9 @@ const ONYXKEYS = { /** This NVP contains the active policyID */ NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID', + /** This NVP contains the referral banners the user dismissed */ + NVP_DISMISSED_REFERRAL_BANNERS: 'dismissedReferralBanners', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -572,6 +575,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; + [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 6db37ce1320a..40c3c8683578 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -8,7 +8,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {DismissedReferralBanners} from '@src/types/onyx/Account'; +import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; import Icon from './Icon'; import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; @@ -82,7 +82,6 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data?.dismissedReferralBanners ?? {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, })(ReferralProgramCTA); diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 72393e89ae1a..a1de24da12d4 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -22,7 +22,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {DismissedReferralBanners} from '@src/types/onyx/Account'; +import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; type NewChatPageWithOnyxProps = { /** All reports shared with the user */ @@ -287,8 +287,7 @@ NewChatPage.displayName = 'NewChatPage'; export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data?.dismissedReferralBanners ?? {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2865316b7fd5..1c31806086bd 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -360,8 +360,7 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTempora export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data.dismissedReferralBanners || {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..85feafc76fe8 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -371,8 +371,7 @@ MoneyRequestParticipantsSelector.defaultProps = defaultProps; export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data.dismissedReferralBanners || {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index b9236b0e7252..e4d319313136 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -35,7 +35,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {NewRoomForm} from '@src/types/form/NewRoomForm'; import INPUT_IDS from '@src/types/form/NewRoomForm'; -import type {Account, Policy, Report as ReportType, Session} from '@src/types/onyx'; +import type {Policy, Report as ReportType, Session} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -53,7 +53,7 @@ type WorkspaceNewRoomPageOnyxProps = { session: OnyxEntry; /** policyID for main workspace */ - activePolicyID: OnyxEntry['activePolicyID']>; + activePolicyID: OnyxEntry>; }; type WorkspaceNewRoomPageProps = WorkspaceNewRoomPageOnyxProps; @@ -144,7 +144,6 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return; } Navigation.dismissModal(newRoomReportID); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State }, [isLoading, errorFields]); useEffect(() => { @@ -342,8 +341,7 @@ export default withOnyx account?.activePolicyID ?? null, + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, initialValue: null, }, })(WorkspaceNewRoomPage); diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts index 534a8ad0f2bc..98ce460a7669 100644 --- a/src/types/onyx/Account.ts +++ b/src/types/onyx/Account.ts @@ -4,14 +4,6 @@ import type * as OnyxCommon from './OnyxCommon'; type TwoFactorAuthStep = ValueOf | ''; -type DismissedReferralBanners = { - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]?: boolean; -}; - type Account = { /** Whether SAML is enabled for the current account */ isSAMLEnabled?: boolean; @@ -64,15 +56,11 @@ type Account = { /** Whether a sign is loading */ isLoading?: boolean; - /** The active policy ID. Initiating a SmartScan will create an expense on this policy by default. */ - activePolicyID?: string; - errors?: OnyxCommon.Errors | null; success?: string; codesAreCopied?: boolean; twoFactorAuthStep?: TwoFactorAuthStep; - dismissedReferralBanners?: DismissedReferralBanners; }; export default Account; -export type {TwoFactorAuthStep, DismissedReferralBanners}; +export type {TwoFactorAuthStep}; diff --git a/src/types/onyx/DismissedReferralBanners.ts b/src/types/onyx/DismissedReferralBanners.ts new file mode 100644 index 000000000000..43fa6472a6ae --- /dev/null +++ b/src/types/onyx/DismissedReferralBanners.ts @@ -0,0 +1,11 @@ +import type CONST from '@src/CONST'; + +type DismissedReferralBanners = { + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]?: boolean; +}; + +export default DismissedReferralBanners; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 6846fc302639..cc9c3cd44831 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; +import type DismissedReferralBanners from './DismissedReferralBanners'; import type Download from './Download'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -85,6 +86,7 @@ export type { Currency, CurrencyList, CustomStatusDraft, + DismissedReferralBanners, Download, FrequentlyUsedEmoji, Fund, From 55f816dd080f2aaf5be2c3dfd90c9ffcb6ebfabd Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 29 Feb 2024 17:10:46 -0300 Subject: [PATCH 016/189] Fix usage of referral banners in account --- src/libs/actions/User.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 5d089ed6e393..ec5991346872 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -961,11 +961,9 @@ function dismissReferralBanner(type: ValueOf Date: Thu, 29 Feb 2024 19:49:19 -0300 Subject: [PATCH 017/189] Suppress some errors --- src/libs/migrations/NVPMigration.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 1c3465a492a9..22bdd4a03615 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -45,9 +45,12 @@ export default function () { key: ONYXKEYS.ACCOUNT, callback: (value) => { Onyx.disconnect(connectionID); + // @ts-expect-error we are removing this property, so it is not in the type anymore if (value?.activePolicyID) { + // @ts-expect-error we are removing this property, so it is not in the type anymore const activePolicyID = value.activePolicyID; - const newValue = value; + const newValue = {...value}; + // @ts-expect-error we are removing this property, so it is not in the type anymore delete newValue.activePolicyID; Onyx.multiSet({ [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, From b17b23cb8306b8820f8d6ab547afb207ec2ab0f3 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 29 Feb 2024 20:05:31 -0300 Subject: [PATCH 018/189] Readd suppression --- src/pages/workspace/WorkspaceNewRoomPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index e4d319313136..9771f8bccae2 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -144,6 +144,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return; } Navigation.dismissModal(newRoomReportID); + // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State }, [isLoading, errorFields]); useEffect(() => { From 89b2e6e14c1a0db6fa88c4b2251c0ac7b35e43b6 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Fri, 1 Mar 2024 11:26:49 +0530 Subject: [PATCH 019/189] update MoneyRequestParticipantsSelector. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 2 +- src/pages/RoomInvitePage.tsx | 2 +- ...yForRefactorRequestParticipantsSelector.js | 42 ++++++++----- .../MoneyRequestParticipantsSelector.js | 63 +++++++++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 9e555b4308b2..c75f8542d901 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -378,7 +378,7 @@ function BaseSelectionList( CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, (e) => { const focusedOption = flattenedSections.allOptions[focusedIndex]; - if (onConfirm && (flattenedSections.selectedOptions.length || focusedOption)) { + if (onConfirm) { onConfirm(e, focusedOption); return; } diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 66d13d496e8c..8be364d9db1c 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -176,7 +176,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter') { + if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2865316b7fd5..f7f64b12f7a4 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -188,15 +188,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ * * @param {Object} option */ - const addSingleParticipant = (option) => { - onParticipantsAdded([ - { - ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), - selected: true, - }, - ]); - onFinish(); - }; + const addSingleParticipant = useCallback( + (option) => { + onParticipantsAdded([ + { + ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), + selected: true, + }, + ]); + onFinish(); + }, + [onFinish, onParticipantsAdded], + ); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. @@ -258,13 +261,22 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; - const handleConfirmSelection = useCallback(() => { - if (shouldShowSplitBillErrorMessage) { - return; - } + const handleConfirmSelection = useCallback( + (keyEvent, option) => { + const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; + if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + return; + } - onFinish(CONST.IOU.TYPE.SPLIT); - }, [shouldShowSplitBillErrorMessage, onFinish]); + if (shouldAddSingleParticipant) { + addSingleParticipant(option); + return; + } + + onFinish(CONST.IOU.TYPE.SPLIT); + }, + [shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants], + ); const footerContent = useMemo( () => ( diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..c55dbeab394b 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -195,25 +195,28 @@ function MoneyRequestParticipantsSelector({ * * @param {Object} option */ - const addSingleParticipant = (option) => { - if (participants.length) { - return; - } - onAddParticipants( - [ - { - accountID: option.accountID, - login: option.login, - isPolicyExpenseChat: option.isPolicyExpenseChat, - reportID: option.reportID, - selected: true, - searchText: option.searchText, - }, - ], - false, - ); - navigateToRequest(); - }; + const addSingleParticipant = useCallback( + (option) => { + if (participants.length) { + return; + } + onAddParticipants( + [ + { + accountID: option.accountID, + login: option.login, + isPolicyExpenseChat: option.isPolicyExpenseChat, + reportID: option.reportID, + selected: true, + searchText: option.searchText, + }, + ], + false, + ); + navigateToRequest(); + }, + [navigateToRequest, onAddParticipants, participants.length], + ); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. @@ -274,13 +277,23 @@ function MoneyRequestParticipantsSelector({ const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; - const handleConfirmSelection = useCallback(() => { - if (shouldShowSplitBillErrorMessage) { - return; - } + const handleConfirmSelection = useCallback( + (keyEvent, option) => { + const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; - navigateToSplit(); - }, [shouldShowSplitBillErrorMessage, navigateToSplit]); + if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + return; + } + + if (shouldAddSingleParticipant) { + addSingleParticipant(option); + return; + } + + navigateToSplit(); + }, + [shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length], + ); const footerContent = useMemo( () => ( diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index b4effc13fa0e..9082dcbf0a80 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -248,7 +248,7 @@ function WorkspaceInvitePage({ const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter') { + if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { From 71e17939f6bef57995d9ba94bc82dc06f35dc886 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Fri, 1 Mar 2024 11:33:07 +0530 Subject: [PATCH 020/189] remove redundant checks Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.tsx | 3 ++- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 4 ++-- .../MoneyRequestParticipantsSelector.js | 4 ++-- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 8be364d9db1c..2f42220dc0d0 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -176,7 +176,8 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { + // if we got + if (option && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f7f64b12f7a4..abe9ab772b40 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -263,8 +263,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const handleConfirmSelection = useCallback( (keyEvent, option) => { - const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; - if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + const shouldAddSingleParticipant = option && !participants.length; + if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { return; } diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index c55dbeab394b..191d80ea99cb 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -279,9 +279,9 @@ function MoneyRequestParticipantsSelector({ const handleConfirmSelection = useCallback( (keyEvent, option) => { - const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; + const shouldAddSingleParticipant = option && !participants.length; - if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { return; } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 9082dcbf0a80..e174d4a365de 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -248,7 +248,7 @@ function WorkspaceInvitePage({ const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { + if (option && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { From 39d33deebea4e1a27bf6a83cf58767755576aaf8 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Fri, 1 Mar 2024 14:14:34 -0300 Subject: [PATCH 021/189] Fix type errors --- src/components/ReferralProgramCTA.tsx | 8 ++++---- src/pages/NewChatPage.tsx | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 40c3c8683578..bd6976c84e3d 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -8,7 +8,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; +import type * as OnyxTypes from '@src/types/onyx'; import Icon from './Icon'; import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; @@ -16,7 +16,7 @@ import Text from './Text'; import Tooltip from './Tooltip'; type ReferralProgramCTAOnyxProps = { - dismissedReferralBanners: DismissedReferralBanners; + dismissedReferralBanners: OnyxEntry; }; type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & { @@ -36,7 +36,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref User.dismissReferralBanner(referralContentType); }; - if (!referralContentType || dismissedReferralBanners[referralContentType]) { + if (!referralContentType || dismissedReferralBanners?.[referralContentType]) { return null; } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index a1de24da12d4..f4eccd52c78e 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -22,7 +22,6 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; type NewChatPageWithOnyxProps = { /** All reports shared with the user */ @@ -34,7 +33,7 @@ type NewChatPageWithOnyxProps = { betas: OnyxEntry; /** An object that holds data about which referral banners have been dismissed */ - dismissedReferralBanners: DismissedReferralBanners; + dismissedReferralBanners: OnyxEntry; /** Whether we are searching for reports in the server */ isSearchingForReports: OnyxEntry; @@ -265,7 +264,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd} shouldShowConfirmButton - shouldShowReferralCTA={!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} + shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')} textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} From 3053b96a9432b9f5161bcfd3a09699e73f8fc86a Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Fri, 1 Mar 2024 14:25:28 -0300 Subject: [PATCH 022/189] More lints --- src/components/ReferralProgramCTA.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index bd6976c84e3d..c93b75bf11ad 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; From 94452f5d83510bf6dec19be805d2a0b1e492ca2c Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 4 Mar 2024 01:24:19 +0100 Subject: [PATCH 023/189] line up checkboxes --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 4e19cba00b2f..cde7eb775f23 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -433,7 +433,7 @@ function BaseSelectionList( ) : ( <> {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - + Date: Mon, 4 Mar 2024 07:41:21 +0530 Subject: [PATCH 024/189] Removed duplicate function --- src/libs/ReportUtils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 11a32aa45b8d..2834240ecd82 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4313,10 +4313,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return !isPolicyExpenseChat(report) || isOwnPolicyExpenseChat; } -function isSelfDM(report: OnyxEntry): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM; -} - /** * Helper method to define what money request options we want to show for particular method. * There are 3 money request options: Request, Split and Send: From 7517fbf5cf3020add121de83952989dc47c57b9e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:26 +0530 Subject: [PATCH 025/189] fixed bad merge commit --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- src/pages/iou/request/step/IOURequestStepConfirmation.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 29ab4d53c55f..629f74205046 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -372,7 +372,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; if (isTypeTrackExpense) { - text = "Track Expense"; + text = 'Track Expense'; } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 73ec541fba4e..de5c6811d277 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -104,8 +104,7 @@ function IOURequestStepConfirmation({ return 'Track Expense'; } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); - } - , [iouType, transaction, translate]); + }, [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { From 6c6690cae4f420e8d5b8e14fc6c417e743fdda36 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:48 +0530 Subject: [PATCH 026/189] fixed bad merge commit --- src/libs/Permissions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 37ef44b80af9..4fef0f15ae49 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -28,6 +28,7 @@ function canUseViolations(betas: OnyxEntry): boolean { function canUseTrackExpense(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas); +} function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas); From d180fd24984f0c92a17b077e1009e048017b9899 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 14:48:11 +0530 Subject: [PATCH 027/189] revert changes in RoomInvitePage & WorkspaceInvitePage. Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.tsx | 52 +++++++-------------- src/pages/workspace/WorkspaceInvitePage.tsx | 26 ++++------- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 06c58f33c812..482ff828e6a8 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -2,7 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {GestureResponderEvent, SectionListData} from 'react-native'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -164,7 +164,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa [selectedOptions], ); - const validate = useCallback((options: ReportUtils.OptionData[]) => options.length > 0, []); + const validate = useCallback(() => selectedOptions.length > 0, [selectedOptions]); // Non policy members should not be able to view the participants of a room const reportID = report?.reportID; @@ -172,40 +172,24 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa const backRoute = useMemo(() => reportID && (isPolicyMember ? ROUTES.ROOM_MEMBERS.getRoute(reportID) : ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)), [isPolicyMember, reportID]); const reportName = useMemo(() => ReportUtils.getReportName(report), [report]); - const inviteUsers = useCallback( - (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { - const options = [...selectedOptions]; - - // if we got - if (option && !options.length) { - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); - - if (option && !isOptionInList) { - toggleOption(option); - options.push(option); - } - } - - if (!validate(options)) { + const inviteUsers = useCallback(() => { + if (!validate()) { + return; + } + const invitedEmailsToAccountIDs: PolicyUtils.MemberEmailsToAccountIDs = {}; + selectedOptions.forEach((option) => { + const login = option.login ?? ''; + const accountID = option.accountID; + if (!login.toLowerCase().trim() || !accountID) { return; } - - const invitedEmailsToAccountIDs: PolicyUtils.MemberEmailsToAccountIDs = {}; - options.forEach((selectedOption) => { - const login = selectedOption.login ?? ''; - const accountID = selectedOption.accountID; - if (!login.toLowerCase().trim() || !accountID) { - return; - } - invitedEmailsToAccountIDs[login] = Number(accountID); - }); - if (reportID) { - Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); - } - Navigation.navigate(backRoute); - }, - [selectedOptions, backRoute, reportID, validate, toggleOption], - ); + invitedEmailsToAccountIDs[login] = Number(accountID); + }); + if (reportID) { + Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); + } + Navigation.navigate(backRoute); + }, [selectedOptions, backRoute, reportID, validate]); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 14c391939005..67bf6f8064da 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,7 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; -import type {GestureResponderEvent, SectionListData} from 'react-native'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -235,9 +235,9 @@ function WorkspaceInvitePage({ setSelectedOptions(newSelectedOptions); }; - const validate = (options: OptionsListUtils.MemberForList[]): boolean => { + const validate = (): boolean => { const errors: Errors = {}; - if (options.length <= 0) { + if (selectedOptions.length <= 0) { errors.noUserSelected = 'true'; } @@ -245,25 +245,15 @@ function WorkspaceInvitePage({ return isEmptyObject(errors); }; - const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { - const options = [...selectedOptions]; - if (option && !options.length) { - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); - - if (option && !isOptionInList) { - toggleOption(option); - options.push(option); - } - } - - if (!validate(options)) { + const inviteUser = () => { + if (!validate()) { return; } const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; - options.forEach((selectedOption) => { - const login = selectedOption.login ?? ''; - const accountID = selectedOption.accountID ?? ''; + selectedOptions.forEach((option) => { + const login = option.login ?? ''; + const accountID = option.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } From 24b28ee2aae23d58e4abd5d00b351aff8bb6380e Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 14:49:01 +0530 Subject: [PATCH 028/189] minor spacing fix. Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 482ff828e6a8..7bcd64397e20 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -171,7 +171,6 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa const isPolicyMember = useMemo(() => (report?.policyID ? PolicyUtils.isPolicyMember(report.policyID, policies as Record) : false), [report?.policyID, policies]); const backRoute = useMemo(() => reportID && (isPolicyMember ? ROUTES.ROOM_MEMBERS.getRoute(reportID) : ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)), [isPolicyMember, reportID]); const reportName = useMemo(() => ReportUtils.getReportName(report), [report]); - const inviteUsers = useCallback(() => { if (!validate()) { return; From cf1aa20d91c338bedb96cc326ce8997bbc74beb0 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 4 Mar 2024 15:11:32 +0530 Subject: [PATCH 029/189] fix: revert removal of onMoveShouldSetPanResponder --- src/components/SwipeInterceptPanResponder.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx index 48cfe4f90c5c..e778f0c49e54 100644 --- a/src/components/SwipeInterceptPanResponder.tsx +++ b/src/components/SwipeInterceptPanResponder.tsx @@ -1,7 +1,8 @@ -import {PanResponder} from 'react-native'; +import { PanResponder } from 'react-native'; const SwipeInterceptPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, onPanResponderTerminationRequest: () => false, }); From aa4d31ab0422c54ee43bf3aa9b8aa925fd19eb03 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 4 Mar 2024 15:24:58 +0530 Subject: [PATCH 030/189] fix: clean lint --- src/components/SwipeInterceptPanResponder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx index e778f0c49e54..6a3d14b3b24b 100644 --- a/src/components/SwipeInterceptPanResponder.tsx +++ b/src/components/SwipeInterceptPanResponder.tsx @@ -1,4 +1,4 @@ -import { PanResponder } from 'react-native'; +import {PanResponder} from 'react-native'; const SwipeInterceptPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, From 8215b5377db03124eed0e166545c5cd2f9d16605 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 4 Mar 2024 12:57:26 +0100 Subject: [PATCH 031/189] address comments --- src/types/onyx/ReportAction.ts | 6 +- src/types/onyx/ReportActionsDrafts.ts | 5 + tests/actions/ReportTest.ts | 45 +++-- tests/unit/APITest.ts | 50 ++--- tests/unit/MigrationTest.ts | 252 +++++++++++++++----------- tests/unit/NetworkTest.ts | 35 ++-- 6 files changed, 228 insertions(+), 165 deletions(-) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index bb5bf50ec6cf..0971fb6b77e1 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as OnyxCommon from './OnyxCommon'; import type {Decision, Reaction} from './OriginalMessage'; @@ -224,5 +226,7 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; +type ReportActionCollectionDataSet = CollectionDataSet; + export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionCollectionDataSet}; diff --git a/src/types/onyx/ReportActionsDrafts.ts b/src/types/onyx/ReportActionsDrafts.ts index 70d16c62a3bc..e4c51c61ed25 100644 --- a/src/types/onyx/ReportActionsDrafts.ts +++ b/src/types/onyx/ReportActionsDrafts.ts @@ -1,5 +1,10 @@ +import type ONYXKEYS from '@src/ONYXKEYS'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type ReportActionsDraft from './ReportActionsDraft'; type ReportActionsDrafts = Record; +type ReportActionsDraftCollectionDataSet = CollectionDataSet; + export default ReportActionsDrafts; +export type {ReportActionsDraftCollectionDataSet}; diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 43ceaaad607e..251d26932128 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -3,17 +3,17 @@ import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/glob import {utcToZonedTime} from 'date-fns-tz'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; +import * as Report from '@src/libs/actions/Report'; +import * as User from '@src/libs/actions/User'; +import DateUtils from '@src/libs/DateUtils'; +import Log from '@src/libs/Log'; +import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import CONST from '../../src/CONST'; -import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; -import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; -import * as Report from '../../src/libs/actions/Report'; -import * as User from '../../src/libs/actions/User'; -import DateUtils from '../../src/libs/DateUtils'; -import Log from '../../src/libs/Log'; -import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; import getIsUsingFakeTimers from '../utils/getIsUsingFakeTimers'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -21,8 +21,8 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; const UTC = 'UTC'; -jest.mock('../../src/libs/actions/Report', () => { - const originalModule: typeof Report = jest.requireActual('../../src/libs/actions/Report'); +jest.mock('@src/libs/actions/Report', () => { + const originalModule = jest.requireActual('@src/libs/actions/Report'); return { ...originalModule, @@ -36,7 +36,6 @@ describe('actions/Report', () => { PusherHelper.setup(); Onyx.init({ keys: ONYXKEYS, - // registerStorageEventListener: () => {}, }); }); @@ -53,7 +52,8 @@ describe('actions/Report', () => { afterEach(PusherHelper.teardown); it('should store a new report action in Onyx when onyxApiUpdate event is handled via Pusher', () => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; @@ -89,7 +89,7 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - const resultAction: OnyxEntry = Object.values(reportActions ?? [])[0]; + const resultAction: OnyxEntry = Object.values(reportActions ?? {})[0]; reportActionID = resultAction.reportActionID; expect(resultAction.message).toEqual(REPORT_ACTION.message); @@ -168,7 +168,8 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) .then(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); // WHEN we add enough logs to send a packet for (let i = 0; i <= LOGGER_MAX_LOG_LINES; i++) { @@ -194,7 +195,8 @@ describe('actions/Report', () => { it('should be updated correctly when new comments are added, deleted or marked as unread', () => { jest.useFakeTimers(); - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const REPORT_ID = '1'; let report: OnyxEntry; let reportActionCreatedDate: string; @@ -427,7 +429,8 @@ describe('actions/Report', () => { * already in the comment and the user deleted it on purpose. */ - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); // User edits comment to add link // We should generate link @@ -539,7 +542,8 @@ describe('actions/Report', () => { }); it('should properly toggle reactions on a message', () => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; @@ -659,7 +663,8 @@ describe('actions/Report', () => { }); it("shouldn't add the same reaction twice when changing preferred skin color and reaction doesn't support skin colors", () => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 9c94730fb4cc..359288b2a1ef 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -1,23 +1,23 @@ -// import Onyx from 'react-native-onyx'; +import MockedOnyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; -import CONST from '../../src/CONST'; -import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; -import * as API from '../../src/libs/API'; -import HttpUtils from '../../src/libs/HttpUtils'; -import * as MainQueue from '../../src/libs/Network/MainQueue'; -import * as NetworkStore from '../../src/libs/Network/NetworkStore'; -import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; -import * as Request from '../../src/libs/Request'; -import * as RequestThrottle from '../../src/libs/RequestThrottle'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; +import * as API from '@src/libs/API'; +import HttpUtils from '@src/libs/HttpUtils'; +import * as MainQueue from '@src/libs/Network/MainQueue'; +import * as NetworkStore from '@src/libs/Network/NetworkStore'; +import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; +import * as Request from '@src/libs/Request'; +import * as RequestThrottle from '@src/libs/RequestThrottle'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; -const Onyx = reactNativeOnyxMock; +const Onyx = MockedOnyx as typeof ReactNativeOnyxMock; -jest.mock('../../src/libs/Log'); +jest.mock('@src/libs/Log'); Onyx.init({ keys: ONYXKEYS, @@ -27,14 +27,21 @@ type Response = { ok?: boolean; status?: ValueOf | ValueOf; jsonCode?: ValueOf; + json?: () => Promise; title?: ValueOf; type?: ValueOf; }; +type XhrCalls = Array<{ + resolve: (value: Response | PromiseLike) => void; + reject: (value: unknown) => void; +}>; + const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); @@ -136,10 +143,7 @@ describe('APITests', () => { test('Write request should not be cleared until a backend response occurs', () => { // We're setting up xhr handler that will resolve calls programmatically - const xhrCalls: Array<{ - resolve: (value: Response | PromiseLike) => void; - reject: (value: unknown) => void; - }> = []; + const xhrCalls: XhrCalls = []; const promises: Array> = []; jest.spyOn(HttpUtils, 'xhr').mockImplementation(() => { @@ -205,8 +209,8 @@ describe('APITests', () => { // Given a retry response create a mock and run some expectations for retrying requests - const retryExpectations = (Response: Response) => { - const successfulResponse = { + const retryExpectations = (response: Response) => { + const successfulResponse: Response = { ok: true, jsonCode: CONST.JSON_CODE.SUCCESS, // We have to mock response.json() too @@ -214,7 +218,7 @@ describe('APITests', () => { }; // Given a mock where a retry response is returned twice before a successful response - global.fetch = jest.fn().mockResolvedValueOnce(Response).mockResolvedValueOnce(Response).mockResolvedValueOnce(successfulResponse); + global.fetch = jest.fn().mockResolvedValueOnce(response).mockResolvedValueOnce(response).mockResolvedValueOnce(successfulResponse); // Given we have a request made while we're offline return ( @@ -275,7 +279,7 @@ describe('APITests', () => { test('write requests are retried when Auth is down', () => { // Given the response data returned when auth is down - const responseData = { + const responseData: Response = { ok: true, status: CONST.JSON_CODE.SUCCESS, jsonCode: CONST.JSON_CODE.EXP_ERROR, diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index 6d18ec2f0c68..bd1f79b8f838 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -1,14 +1,17 @@ /* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; -import Log from '../../src/libs/Log'; -import CheckForPreviousReportActionID from '../../src/libs/migrations/CheckForPreviousReportActionID'; -import KeyReportActionsDraftByReportActionID from '../../src/libs/migrations/KeyReportActionsDraftByReportActionID'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import Log from '@src/libs/Log'; +import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviousReportActionID'; +import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActionCollectionDataSet} from '@src/types/onyx/ReportAction'; +import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -jest.mock('../../src/libs/getPlatform'); +jest.mock('@src/libs/getPlatform'); -let LogSpy: unknown; +let LogSpy: jest.SpyInstance>; describe('Migrations', () => { beforeAll(() => { @@ -30,18 +33,23 @@ describe('Migrations', () => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no reportActions'), )); - it('Should remove all report actions given that a previousReportActionID does not exist', () => - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { - // @ts-expect-error Preset necessary values - 1: { - reportActionID: '1', - }, - 2: { - reportActionID: '2', - }, + it('Should remove all report actions given that a previousReportActionID does not exist', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -56,22 +64,28 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); - })); - - it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { - // @ts-expect-error Preset necessary values - 1: { - reportActionID: '1', - previousReportActionID: '0', - }, - 2: { - reportActionID: '2', - previousReportActionID: '1', - }, + }); + }); + + it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { + 1: { + reportActionID: '1', + previousReportActionID: '0', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + previousReportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -93,23 +107,33 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); - })); - - it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: { - 1: { - reportActionID: '1', - }, - 2: { - reportActionID: '2', - }, + }); + }); + + it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; + + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -127,25 +151,34 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); }, }); - })); - - it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: { - 1: { - reportActionID: '1', - previousReportActionID: '10', - }, - 2: { - reportActionID: '2', - previousReportActionID: '23', - }, + }); + }); + + it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { + 1: { + reportActionID: '1', + previousReportActionID: '10', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + previousReportActionID: '23', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -171,16 +204,20 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); }, }); - })); - - it('Should skip if no valid reportActions', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: null, - }) + }); + }); + + it('Should skip if no valid reportActions', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = null; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = {}; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = {}; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = null; + + Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no valid reportActions'); @@ -196,7 +233,8 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); }, }); - })); + }); + }); }); describe('KeyReportActionsDraftByReportActionID', () => { @@ -205,14 +243,15 @@ describe('Migrations', () => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration KeyReportActionsDraftByReportActionID because there were no reportActionsDrafts'), )); - it('Should move individual draft to a draft collection of report', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: 'a', - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: 'b', - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]: {3: 'c'}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]: 'd', - }) + it('Should move individual draft to a draft collection of report', () => { + const setQueries: ReportActionsDraftCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = 'a'; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = 'b'; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {3: 'c'}; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = 'd'; + + Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ @@ -235,16 +274,18 @@ describe('Migrations', () => { expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft2); }, }); - })); - - it('Should skip if nothing to migrate', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]: null, - }) + }); + }); + + it('Should skip if nothing to migrate', () => { + const setQueries: ReportActionsDraftCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = null; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = null; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {}; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = null; + + Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration KeyReportActionsDraftByReportActionID because there are no actions drafts to migrate'); @@ -260,14 +301,16 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft); }, }); - })); - - it("Shouldn't move empty individual draft to a draft collection of report", () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: '', - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]: {}, - }) + }); + }); + + it("Shouldn't move empty individual draft to a draft collection of report", () => { + const setQueries: ReportActionsDraftCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = ''; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`] = {}; + + Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ @@ -278,6 +321,7 @@ describe('Migrations', () => { expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); }, }); - })); + }); + }); }); }); diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index f8b5b6a7d345..63b275a1a6b6 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -1,24 +1,24 @@ import type {Mock} from 'jest-mock'; -import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; -// import Onyx from 'react-native-onyx'; -import CONST from '../../src/CONST'; -import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; -import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; -import * as PersonalDetails from '../../src/libs/actions/PersonalDetails'; -import * as Session from '../../src/libs/actions/Session'; -import HttpUtils from '../../src/libs/HttpUtils'; -import Log from '../../src/libs/Log'; -import * as Network from '../../src/libs/Network'; -import * as MainQueue from '../../src/libs/Network/MainQueue'; -import * as NetworkStore from '../../src/libs/Network/NetworkStore'; -import NetworkConnection from '../../src/libs/NetworkConnection'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import MockedOnyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; +import * as PersonalDetails from '@src/libs/actions/PersonalDetails'; +import * as Session from '@src/libs/actions/Session'; +import HttpUtils from '@src/libs/HttpUtils'; +import Log from '@src/libs/Log'; +import * as Network from '@src/libs/Network'; +import * as MainQueue from '@src/libs/Network/MainQueue'; +import * as NetworkStore from '@src/libs/Network/NetworkStore'; +import NetworkConnection from '@src/libs/NetworkConnection'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const Onyx = reactNativeOnyxMock; +const Onyx = MockedOnyx as typeof ReactNativeOnyxMock; -jest.mock('../../src/libs/Log'); +jest.mock('@src/libs/Log'); Onyx.init({ keys: ONYXKEYS, @@ -28,7 +28,8 @@ OnyxUpdateManager(); const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); From e0813e48574bc22e8a14844d8fae4afcd7c86f20 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 4 Mar 2024 14:09:18 +0100 Subject: [PATCH 032/189] fix test --- tests/unit/MigrationTest.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index bd1f79b8f838..d60761cd1d89 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -178,7 +178,7 @@ describe('Migrations', () => { }, }; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -217,7 +217,7 @@ describe('Migrations', () => { // @ts-expect-error preset null value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = null; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no valid reportActions'); @@ -225,8 +225,8 @@ describe('Migrations', () => { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, callback: (allReportActions) => { - Onyx.disconnect(connectionID); const expectedReportAction = {}; + Onyx.disconnect(connectionID); expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); @@ -246,12 +246,15 @@ describe('Migrations', () => { it('Should move individual draft to a draft collection of report', () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; + // @ts-expect-error preset invalid value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = 'a'; + // @ts-expect-error preset invalid value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = 'b'; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {3: 'c'}; + // @ts-expect-error preset invalid value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = 'd'; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ @@ -280,12 +283,9 @@ describe('Migrations', () => { it('Should skip if nothing to migrate', () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = null; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = null; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {}; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = null; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration KeyReportActionsDraftByReportActionID because there are no actions drafts to migrate'); @@ -307,10 +307,11 @@ describe('Migrations', () => { it("Shouldn't move empty individual draft to a draft collection of report", () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; + // @ts-expect-error preset empty string value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = ''; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`] = {}; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ From a2ada45f0e5ee307f0d8b6073b6dacecabe47256 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:36:54 -0300 Subject: [PATCH 033/189] Migrate recently used tags too --- src/ONYXKEYS.ts | 2 +- src/libs/migrations/NVPMigration.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index db9864e6800c..1087312a4acd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -293,7 +293,7 @@ const ONYXKEYS = { POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', - POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', + POLICY_RECENTLY_USED_TAGS: 'nvp_policyRecentlyUsedTags_', POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 22bdd4a03615..a6fe81fa0aee 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -21,8 +21,8 @@ const migrations = { // This migration changes the keys of all the NVP related keys so that they are standardized export default function () { return new Promise((resolve) => { - // It's 1 more because activePolicyID is not in the migrations object above as it is nested inside an object - const resolveWhenDone = after(Object.entries(migrations).length + 1, () => resolve()); + // We add the number of manual connections we add below + const resolveWhenDone = after(Object.entries(migrations).length + 2, () => resolve()); for (const [oldKey, newKey] of Object.entries(migrations)) { const connectionID = Onyx.connect({ @@ -41,10 +41,10 @@ export default function () { }, }); } - const connectionID = Onyx.connect({ + const connectionIDAccount = Onyx.connect({ key: ONYXKEYS.ACCOUNT, callback: (value) => { - Onyx.disconnect(connectionID); + Onyx.disconnect(connectionIDAccount); // @ts-expect-error we are removing this property, so it is not in the type anymore if (value?.activePolicyID) { // @ts-expect-error we are removing this property, so it is not in the type anymore @@ -60,5 +60,26 @@ export default function () { resolveWhenDone(); }, }); + const connectionIDRecentlyUsedTags = Onyx.connect({ + // @ts-expect-error The key was renamed, so it does not exist in the type definition + key: 'policyRecentlyUsedTags_', + waitForCollectionCallback: true, + callback: (value) => { + Onyx.disconnect(connectionIDRecentlyUsedTags); + if (!value) { + resolveWhenDone(); + return; + } + const newValue = {}; + for (const key of Object.keys(value)) { + // @ts-expect-error We have no fixed types here + newValue[`nvp_${key}`] = value[key]; + // @ts-expect-error We have no fixed types here + newValue[key] = null; + } + Onyx.multiSet(newValue); + resolveWhenDone(); + }, + }); }); } From f0c591094bbd81300c3c3497750dc871b86830d3 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:47:21 -0300 Subject: [PATCH 034/189] Make collection load properly --- src/ONYXKEYS.ts | 2 ++ src/libs/migrations/NVPMigration.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1087312a4acd..d581e515e0f5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -294,6 +294,7 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', POLICY_RECENTLY_USED_TAGS: 'nvp_policyRecentlyUsedTags_', + OLD_POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', @@ -484,6 +485,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; + [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index a6fe81fa0aee..6be142eb1f2a 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -61,8 +61,7 @@ export default function () { }, }); const connectionIDRecentlyUsedTags = Onyx.connect({ - // @ts-expect-error The key was renamed, so it does not exist in the type definition - key: 'policyRecentlyUsedTags_', + key: ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS, waitForCollectionCallback: true, callback: (value) => { Onyx.disconnect(connectionIDRecentlyUsedTags); From fdadc74041fbcac42c12ee063ab14ded025e2a21 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:57:55 -0300 Subject: [PATCH 035/189] Correct onyx key --- src/ONYXKEYS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d581e515e0f5..13f578dae136 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -199,7 +199,7 @@ const ONYXKEYS = { PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', /** Store frequently used emojis for this user */ - FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', + FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', From 1290c364747c9a61908bb88b36ac75437251204e Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 17:46:36 -0300 Subject: [PATCH 036/189] Add nvp prefix --- src/ONYXKEYS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 13f578dae136..031759c2b4eb 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -132,7 +132,7 @@ const ONYXKEYS = { NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID', /** This NVP contains the referral banners the user dismissed */ - NVP_DISMISSED_REFERRAL_BANNERS: 'dismissedReferralBanners', + NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners', /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', From 7b3c4134c5f03cca3b8f17c5e140d77ea5db1a83 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 6 Mar 2024 11:20:48 +0100 Subject: [PATCH 037/189] Migrate g15 stories to TS --- src/components/CheckboxWithLabel.tsx | 2 + src/components/OptionRow.tsx | 2 + src/components/PopoverMenu.tsx | 2 +- ...ories.js => CheckboxWithLabel.stories.tsx} | 15 +++++--- ...nuItem.stories.js => MenuItem.stories.tsx} | 38 ++++++++++--------- ...onRow.stories.js => OptionRow.stories.tsx} | 3 +- ...enu.stories.js => PopoverMenu.stories.tsx} | 28 +++++++------- ...stories.js => SubscriptAvatar.stories.tsx} | 14 ++++--- 8 files changed, 59 insertions(+), 45 deletions(-) rename src/stories/{CheckboxWithLabel.stories.js => CheckboxWithLabel.stories.tsx} (73%) rename src/stories/{MenuItem.stories.js => MenuItem.stories.tsx} (77%) rename src/stories/{OptionRow.stories.js => OptionRow.stories.tsx} (94%) rename src/stories/{PopoverMenu.stories.js => PopoverMenu.stories.tsx} (78%) rename src/stories/{SubscriptAvatar.stories.js => SubscriptAvatar.stories.tsx} (77%) diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 2919debe9cb1..dd169576186e 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -108,3 +108,5 @@ function CheckboxWithLabel( CheckboxWithLabel.displayName = 'CheckboxWithLabel'; export default React.forwardRef(CheckboxWithLabel); + +export type {CheckboxWithLabelProps}; diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 7b45fd963fe7..97ef6885c80f 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -340,3 +340,5 @@ export default React.memo( prevProps.option.pendingAction === nextProps.option.pendingAction && prevProps.option.customIcon === nextProps.option.customIcon, ); + +export type {OptionRowProps}; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index a391ff061baa..3a211f90bd14 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -243,4 +243,4 @@ function PopoverMenu({ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo(PopoverMenu); -export type {PopoverMenuItem}; +export type {PopoverMenuItem, PopoverMenuProps}; diff --git a/src/stories/CheckboxWithLabel.stories.js b/src/stories/CheckboxWithLabel.stories.tsx similarity index 73% rename from src/stories/CheckboxWithLabel.stories.js rename to src/stories/CheckboxWithLabel.stories.tsx index f978856aaefb..b5e8bc72f380 100644 --- a/src/stories/CheckboxWithLabel.stories.js +++ b/src/stories/CheckboxWithLabel.stories.tsx @@ -1,29 +1,33 @@ +import type {ComponentMeta, ComponentStory} from '@storybook/react'; import React from 'react'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type {CheckboxWithLabelProps} from '@components/CheckboxWithLabel'; import Text from '@components/Text'; // eslint-disable-next-line no-restricted-imports import {defaultStyles} from '@styles/index'; +type CheckboxWithLabelStory = ComponentStory; + /** * We use the Component Story Format for writing stories. Follow the docs here: * * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format */ -const story = { +const story: ComponentMeta = { title: 'Components/CheckboxWithLabel', component: CheckboxWithLabel, }; -function Template(args) { +function Template(args: CheckboxWithLabelProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } // Arguments can be passed to the component by binding // See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default = Template.bind({}); -const WithLabelComponent = Template.bind({}); -const WithErrors = Template.bind({}); +const Default: CheckboxWithLabelStory = Template.bind({}); +const WithLabelComponent: CheckboxWithLabelStory = Template.bind({}); +const WithErrors: CheckboxWithLabelStory = Template.bind({}); Default.args = { isChecked: true, label: 'Plain text label', @@ -44,7 +48,6 @@ WithLabelComponent.args = { WithErrors.args = { isChecked: false, - hasError: true, errorText: 'Please accept Terms before continuing.', onInputChange: () => {}, label: 'I accept the Terms & Conditions', diff --git a/src/stories/MenuItem.stories.js b/src/stories/MenuItem.stories.tsx similarity index 77% rename from src/stories/MenuItem.stories.js rename to src/stories/MenuItem.stories.tsx index 0e7260fa4d1a..4e02bcaf785f 100644 --- a/src/stories/MenuItem.stories.js +++ b/src/stories/MenuItem.stories.tsx @@ -1,26 +1,30 @@ +import type {ComponentMeta, ComponentStory} from '@storybook/react'; import React from 'react'; import Chase from '@assets/images/bankicons/chase.svg'; import MenuItem from '@components/MenuItem'; +import type {MenuItemProps} from '@components/MenuItem'; import variables from '@styles/variables'; +type MenuItemStory = ComponentStory; + /** * We use the Component Story Format for writing stories. Follow the docs here: * * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format */ -const story = { +const story: ComponentMeta = { title: 'Components/MenuItem', component: MenuItem, }; -function Template(args) { +function Template(args: MenuItemProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } // Arguments can be passed to the component by binding // See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default = Template.bind({}); +const Default: MenuItemStory = Template.bind({}); Default.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -28,7 +32,7 @@ Default.args = { iconWidth: variables.iconSizeExtraLarge, }; -const Description = Template.bind({}); +const Description: MenuItemStory = Template.bind({}); Description.args = { title: 'Alberta Bobbeth Charleson', description: 'Account ending in 1111', @@ -37,7 +41,7 @@ Description.args = { iconWidth: variables.iconSizeExtraLarge, }; -const RightIcon = Template.bind({}); +const RightIcon: MenuItemStory = Template.bind({}); RightIcon.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -46,7 +50,7 @@ RightIcon.args = { shouldShowRightIcon: true, }; -const RightIconAndDescription = Template.bind({}); +const RightIconAndDescription: MenuItemStory = Template.bind({}); RightIconAndDescription.args = { title: 'Alberta Bobbeth Charleson', description: 'Account ending in 1111', @@ -56,7 +60,7 @@ RightIconAndDescription.args = { shouldShowRightIcon: true, }; -const RightIconAndDescriptionWithLabel = Template.bind({}); +const RightIconAndDescriptionWithLabel: MenuItemStory = Template.bind({}); RightIconAndDescriptionWithLabel.args = { label: 'Account number', title: 'Alberta Bobbeth Charleson', @@ -67,7 +71,7 @@ RightIconAndDescriptionWithLabel.args = { shouldShowRightIcon: true, }; -const Selected = Template.bind({}); +const Selected: MenuItemStory = Template.bind({}); Selected.args = { title: 'Alberta Bobbeth Charleson', description: 'Account ending in 1111', @@ -78,7 +82,7 @@ Selected.args = { isSelected: true, }; -const BadgeText = Template.bind({}); +const BadgeText: MenuItemStory = Template.bind({}); BadgeText.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -88,7 +92,7 @@ BadgeText.args = { badgeText: '$0.00', }; -const Focused = Template.bind({}); +const Focused: MenuItemStory = Template.bind({}); Focused.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -98,7 +102,7 @@ Focused.args = { focused: true, }; -const Disabled = Template.bind({}); +const Disabled: MenuItemStory = Template.bind({}); Disabled.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -108,17 +112,17 @@ Disabled.args = { disabled: true, }; -const BrickRoadIndicatorSuccess = Template.bind({}); -BrickRoadIndicatorSuccess.args = { +const BrickRoadIndicatorInfo: MenuItemStory = Template.bind({}); +BrickRoadIndicatorInfo.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, iconHeight: variables.iconSizeExtraLarge, iconWidth: variables.iconSizeExtraLarge, shouldShowRightIcon: true, - brickRoadIndicator: 'success', + brickRoadIndicator: 'info', }; -const BrickRoadIndicatorFailure = Template.bind({}); +const BrickRoadIndicatorFailure: MenuItemStory = Template.bind({}); BrickRoadIndicatorFailure.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -128,7 +132,7 @@ BrickRoadIndicatorFailure.args = { brickRoadIndicator: 'error', }; -const ErrorMessage = Template.bind({}); +const ErrorMessage: MenuItemStory = Template.bind({}); ErrorMessage.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -149,7 +153,7 @@ export { BadgeText, Focused, Disabled, - BrickRoadIndicatorSuccess, + BrickRoadIndicatorInfo, BrickRoadIndicatorFailure, RightIconAndDescriptionWithLabel, ErrorMessage, diff --git a/src/stories/OptionRow.stories.js b/src/stories/OptionRow.stories.tsx similarity index 94% rename from src/stories/OptionRow.stories.js rename to src/stories/OptionRow.stories.tsx index 3096940dda5f..d2fffcd583dd 100644 --- a/src/stories/OptionRow.stories.js +++ b/src/stories/OptionRow.stories.tsx @@ -2,6 +2,7 @@ import React from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import OnyxProvider from '@components/OnyxProvider'; import OptionRow from '@components/OptionRow'; +import type {OptionRowProps} from '@components/OptionRow'; /* eslint-disable react/jsx-props-no-spreading */ @@ -42,7 +43,7 @@ export default { }, }; -function Template(args) { +function Template(args: OptionRowProps) { return ( diff --git a/src/stories/PopoverMenu.stories.js b/src/stories/PopoverMenu.stories.tsx similarity index 78% rename from src/stories/PopoverMenu.stories.js rename to src/stories/PopoverMenu.stories.tsx index c03a554741f1..2f1491bdd5f3 100644 --- a/src/stories/PopoverMenu.stories.js +++ b/src/stories/PopoverMenu.stories.tsx @@ -1,36 +1,40 @@ +import type {ComponentMeta, ComponentStory} from '@storybook/react'; import React from 'react'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import PopoverMenu from '@components/PopoverMenu'; +import type {PopoverMenuProps} from '@components/PopoverMenu'; // eslint-disable-next-line no-restricted-imports import themeColors from '@styles/theme/themes/dark'; +type PopoverMenuStory = ComponentStory; + /** * We use the Component Story Format for writing stories. Follow the docs here: * * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format */ -const story = { +const story: ComponentMeta = { title: 'Components/PopoverMenu', component: PopoverMenu, }; -function Template(args) { +function Template(args: PopoverMenuProps) { const [isVisible, setIsVisible] = React.useState(false); const toggleVisibility = () => setIsVisible(!isVisible); return ( <> ; + /** * We use the Component Story Format for writing stories. Follow the docs here: * @@ -23,27 +27,27 @@ export default { }, }; -function Template(args) { +function Template(args: SubscriptAvatarProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } // Arguments can be passed to the component by binding // See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default = Template.bind({}); +const Default: SubscriptAvatarStory = Template.bind({}); -const AvatarURLStory = Template.bind({}); +const AvatarURLStory: SubscriptAvatarStory = Template.bind({}); AvatarURLStory.args = { mainAvatar: {source: defaultAvatars.Avatar1, name: '', type: CONST.ICON_TYPE_AVATAR}, secondaryAvatar: {source: defaultAvatars.Avatar3, name: '', type: CONST.ICON_TYPE_AVATAR}, }; -const SubscriptIcon = Template.bind({}); +const SubscriptIcon: SubscriptAvatarStory = Template.bind({}); SubscriptIcon.args = { subscriptIcon: {source: Expensicons.DownArrow, width: 8, height: 8}, }; -const WorkspaceSubscriptIcon = Template.bind({}); +const WorkspaceSubscriptIcon: SubscriptAvatarStory = Template.bind({}); WorkspaceSubscriptIcon.args = { mainAvatar: {source: defaultAvatars.Avatar1, name: '', type: CONST.ICON_TYPE_WORKSPACE}, subscriptIcon: {source: Expensicons.DownArrow, width: 8, height: 8}, From fe7c953e9fd0206dd5dd308a5cd5e2d8c4309613 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 6 Mar 2024 11:43:30 +0100 Subject: [PATCH 038/189] Rename args to props --- src/stories/CheckboxWithLabel.stories.tsx | 4 ++-- src/stories/MenuItem.stories.tsx | 4 ++-- src/stories/OptionRow.stories.tsx | 4 ++-- src/stories/PopoverMenu.stories.tsx | 4 ++-- src/stories/SubscriptAvatar.stories.tsx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/stories/CheckboxWithLabel.stories.tsx b/src/stories/CheckboxWithLabel.stories.tsx index b5e8bc72f380..8d3c1610e500 100644 --- a/src/stories/CheckboxWithLabel.stories.tsx +++ b/src/stories/CheckboxWithLabel.stories.tsx @@ -18,9 +18,9 @@ const story: ComponentMeta = { component: CheckboxWithLabel, }; -function Template(args: CheckboxWithLabelProps) { +function Template(props: CheckboxWithLabelProps) { // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } // Arguments can be passed to the component by binding diff --git a/src/stories/MenuItem.stories.tsx b/src/stories/MenuItem.stories.tsx index 4e02bcaf785f..da486656cddf 100644 --- a/src/stories/MenuItem.stories.tsx +++ b/src/stories/MenuItem.stories.tsx @@ -17,9 +17,9 @@ const story: ComponentMeta = { component: MenuItem, }; -function Template(args: MenuItemProps) { +function Template(props: MenuItemProps) { // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } // Arguments can be passed to the component by binding diff --git a/src/stories/OptionRow.stories.tsx b/src/stories/OptionRow.stories.tsx index d2fffcd583dd..ea83816ab340 100644 --- a/src/stories/OptionRow.stories.tsx +++ b/src/stories/OptionRow.stories.tsx @@ -43,10 +43,10 @@ export default { }, }; -function Template(args: OptionRowProps) { +function Template(props: OptionRowProps) { return ( - + ); } diff --git a/src/stories/PopoverMenu.stories.tsx b/src/stories/PopoverMenu.stories.tsx index 2f1491bdd5f3..8396a0ea15b5 100644 --- a/src/stories/PopoverMenu.stories.tsx +++ b/src/stories/PopoverMenu.stories.tsx @@ -20,7 +20,7 @@ const story: ComponentMeta = { component: PopoverMenu, }; -function Template(args: PopoverMenuProps) { +function Template(props: PopoverMenuProps) { const [isVisible, setIsVisible] = React.useState(false); const toggleVisibility = () => setIsVisible(!isVisible); return ( @@ -34,7 +34,7 @@ function Template(args: PopoverMenuProps) { ; + return ; } // Arguments can be passed to the component by binding From 54c7a4cb0d2dae6e9f56761c10f67d6040c43b4c Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Wed, 6 Mar 2024 13:34:50 -0300 Subject: [PATCH 039/189] Early return, move NVP constants, only resolve promise when set is done --- src/ONYXKEYS.ts | 39 ++++++++++++++------------- src/libs/migrations/NVPMigration.ts | 42 +++++++++++++++-------------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 031759c2b4eb..33f38e0f5c91 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -16,9 +16,6 @@ const ONYXKEYS = { /** Holds the reportID for the report between the user and their account manager */ ACCOUNT_MANAGER_REPORT_ID: 'accountManagerReportID', - /** Boolean flag only true when first set */ - NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', - /** Holds an array of client IDs which is used for multi-tabs on web in order to know * which tab is the leader, and which ones are the followers */ ACTIVE_CLIENTS: 'activeClients', @@ -106,7 +103,11 @@ const ONYXKEYS = { STASHED_SESSION: 'stashedSession', BETAS: 'betas', - /** NVP keys + /** NVP keys */ + + /** Boolean flag only true when first set */ + NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', + /** Contains the user preference for the LHN priority mode */ NVP_PRIORITY_MODE: 'nvp_priorityMode', @@ -134,6 +135,21 @@ const ONYXKEYS = { /** This NVP contains the referral banners the user dismissed */ NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners', + /** Indicates which locale should be used */ + NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', + + /** Whether the user has tried focus mode yet */ + NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', + + /** Whether the user has been shown the hold educational interstitial yet */ + NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', + + /** Store preferred skintone for emoji */ + PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', + + /** Store frequently used emojis for this user */ + FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -153,9 +169,6 @@ const ONYXKEYS = { ONFIDO_TOKEN: 'onfidoToken', ONFIDO_APPLICANT_ID: 'onfidoApplicantID', - /** Indicates which locale should be used */ - NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', - /** User's Expensify Wallet */ USER_WALLET: 'userWallet', @@ -177,12 +190,6 @@ const ONYXKEYS = { /** The user's cash card and imported cards (including the Expensify Card) */ CARD_LIST: 'cardList', - /** Whether the user has tried focus mode yet */ - NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', - - /** Whether the user has been shown the hold educational interstitial yet */ - NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', - /** Boolean flag used to display the focus mode notification */ FOCUS_MODE_NOTIFICATION: 'focusModeNotification', @@ -195,12 +202,6 @@ const ONYXKEYS = { /** Stores information about the active reimbursement account being set up */ REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', - /** Store preferred skintone for emoji */ - PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', - - /** Store frequently used emojis for this user */ - FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', - /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 6be142eb1f2a..26375c1858eb 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -2,6 +2,7 @@ import after from 'lodash/after'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +// These are the oldKeyName: newKeyName of the NVPs we can migrate without any processing const migrations = { // eslint-disable-next-line @typescript-eslint/naming-convention nvp_lastPaymentMethod: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, @@ -30,14 +31,15 @@ export default function () { key: oldKey, callback: (value) => { Onyx.disconnect(connectionID); - if (value !== null) { - // @ts-expect-error These keys are variables, so we can't check the type - Onyx.multiSet({ - [newKey]: value, - [oldKey]: null, - }); + if (value === null) { + resolveWhenDone(); + return; } - resolveWhenDone(); + // @ts-expect-error These keys are variables, so we can't check the type + Onyx.multiSet({ + [newKey]: value, + [oldKey]: null, + }).then(resolveWhenDone); }, }); } @@ -46,18 +48,19 @@ export default function () { callback: (value) => { Onyx.disconnect(connectionIDAccount); // @ts-expect-error we are removing this property, so it is not in the type anymore - if (value?.activePolicyID) { - // @ts-expect-error we are removing this property, so it is not in the type anymore - const activePolicyID = value.activePolicyID; - const newValue = {...value}; - // @ts-expect-error we are removing this property, so it is not in the type anymore - delete newValue.activePolicyID; - Onyx.multiSet({ - [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, - [ONYXKEYS.ACCOUNT]: newValue, - }); + if (!value?.activePolicyID) { + resolveWhenDone(); + return; } - resolveWhenDone(); + // @ts-expect-error we are removing this property, so it is not in the type anymore + const activePolicyID = value.activePolicyID; + const newValue = {...value}; + // @ts-expect-error we are removing this property, so it is not in the type anymore + delete newValue.activePolicyID; + Onyx.multiSet({ + [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, + [ONYXKEYS.ACCOUNT]: newValue, + }).then(resolveWhenDone); }, }); const connectionIDRecentlyUsedTags = Onyx.connect({ @@ -76,8 +79,7 @@ export default function () { // @ts-expect-error We have no fixed types here newValue[key] = null; } - Onyx.multiSet(newValue); - resolveWhenDone(); + Onyx.multiSet(newValue).then(resolveWhenDone); }, }); }); From be58c4f67eaf2c6075da772f4554cc8693c99d3b Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Wed, 6 Mar 2024 20:44:41 +0100 Subject: [PATCH 040/189] Update src/libs/migrations/NVPMigration.ts Co-authored-by: Tim Golen --- src/libs/migrations/NVPMigration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 26375c1858eb..9ab774328f78 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -22,7 +22,7 @@ const migrations = { // This migration changes the keys of all the NVP related keys so that they are standardized export default function () { return new Promise((resolve) => { - // We add the number of manual connections we add below + // Resolve the migration when all the keys have been migrated. The number of keys is the size of the `migrations` object in addition to the ACCOUNT and OLD_POLICY_RECENTLY_USED_TAGS keys (which is why there is a +2). const resolveWhenDone = after(Object.entries(migrations).length + 2, () => resolve()); for (const [oldKey, newKey] of Object.entries(migrations)) { From 71dcc0364383f10e39cd8f0b05f9ba9c8459b1c2 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 15:52:09 +0700 Subject: [PATCH 041/189] fix in app sound is played if user not viewing chat --- src/libs/actions/User.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 708fc5e8591d..e347fddfb4a7 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -30,6 +30,7 @@ import PusherUtils from '@libs/PusherUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile'; +import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -489,7 +490,11 @@ const isChannelMuted = (reportId: string) => function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { const reportActionsOnly = pushJSON.filter((update) => update.key?.includes('reportActions_')); // "reportActions_5134363522480668" -> "5134363522480668" - const reportIDs = reportActionsOnly.map((value) => value.key.split('_')[1]); + const reportIDs = reportActionsOnly + .map((value) => value.key.split('_')[1]) + .filter((reportID) => { + return reportID === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus(); + }); Promise.all(reportIDs.map((reportID) => isChannelMuted(reportID))) .then((muted) => muted.every((isMuted) => isMuted)) From 82e1fe56e4710afdb0a30824cdc6c736c3ab9aeb Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 14:41:24 +0530 Subject: [PATCH 042/189] Completing UI changes in Request for Track Expense --- ...oraryForRefactorRequestConfirmationList.js | 2 +- src/components/OptionRow.tsx | 7 +++- src/components/ReportWelcomeText.tsx | 10 +++--- src/languages/en.ts | 3 ++ src/languages/es.ts | 3 ++ src/libs/OptionsListUtils.ts | 32 +++++++++++++++++++ src/libs/ReportUtils.ts | 23 +++++++++++-- src/libs/actions/IOU.ts | 10 +++--- .../AttachmentPickerWithMenuItems.tsx | 11 +++++-- .../FloatingActionButtonAndPopover.js | 4 +-- src/pages/iou/request/IOURequestStartPage.js | 2 +- .../iou/request/step/IOURequestStepAmount.js | 2 +- .../step/IOURequestStepConfirmation.js | 15 +++------ .../request/step/IOURequestStepDistance.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- .../step/IOURequestStepTaxAmountPage.js | 1 + 17 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index a3d61e5ad813..e60c99fce6d7 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -381,7 +381,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; if (isTypeTrackExpense) { - text = 'Track Expense'; + text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 7b45fd963fe7..61319de5c56b 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -229,7 +229,12 @@ function OptionRow({ numberOfLines={isMultilineSupported ? 2 : 1} textStyles={displayNameStyle} shouldUseFullTitle={ - !!option.isChatRoom || !!option.isPolicyExpenseChat || !!option.isMoneyRequestReport || !!option.isThread || !!option.isTaskReport + !!option.isChatRoom || + !!option.isPolicyExpenseChat || + !!option.isMoneyRequestReport || + !!option.isThread || + !!option.isTaskReport || + !!option.isSelfDM } /> {option.alternateText ? ( diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index e9bbd0f27bdc..219199c25bc3 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -33,6 +34,7 @@ type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & { function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {canUseTrackExpense} = usePermissions(); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isChatRoom = ReportUtils.isChatRoom(report); const isSelfDM = ReportUtils.isSelfDM(report); @@ -42,7 +44,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense); const additionalText = moneyRequestOptions.map((item) => translate(`reportActionsView.iouTypes.${item}`)).join(', '); const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); const reportName = ReportUtils.getReportName(report); @@ -158,9 +160,9 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP ))} )} - {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && ( - {translate('reportActionsView.usePlusButton', {additionalText})} - )} + {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || + moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST) || + moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)) && {translate('reportActionsView.usePlusButton', {additionalText})}} ); diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a52cca62ef5..53a7758799d3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -503,6 +503,8 @@ export default { send: 'send money', split: 'split a bill', request: 'request money', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'track-expense': 'track an expense', }, }, reportAction: { @@ -592,6 +594,7 @@ export default { participants: 'Participants', requestMoney: 'Request money', sendMoney: 'Send money', + trackExpense: 'Track expense', pay: 'Pay', cancelPayment: 'Cancel payment', cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 013255c1e11e..aaee08c4a9e9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -496,6 +496,8 @@ export default { send: 'enviar dinero', split: 'dividir una factura', request: 'pedir dinero', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'track-expense': 'rastrear un gasto', }, }, reportAction: { @@ -585,6 +587,7 @@ export default { participants: 'Participantes', requestMoney: 'Pedir dinero', sendMoney: 'Enviar dinero', + trackExpense: 'Seguimiento de gastos', pay: 'Pagar', cancelPayment: 'Cancelar el pago', cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 07f0df962455..dbe60d04b45b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -249,9 +249,11 @@ Onyx.connect({ }); const policyExpenseReports: OnyxCollection = {}; +const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { + allReports[key] = report; if (!ReportUtils.isPolicyExpenseChat(report)) { return; } @@ -738,6 +740,35 @@ function createOption( return result; } +/** + * Get the option for a given report. + */ +function getReportOption(participant: Participant): ReportUtils.OptionData { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`]; + + const option = createOption( + report?.visibleChatMemberAccountIDs ?? [], + allPersonalDetails ?? {}, + report ?? null, + {}, + { + showChatPreviewLine: false, + forcePolicyNamePreview: false, + }, + ); + + // Update text & alternateText because createOption returns workspace name only if report is owned by the user + if (option.isSelfDM) { + option.alternateText = Localize.translateLocal('reportActionsView.yourSpace'); + } else { + option.text = ReportUtils.getPolicyName(report); + option.alternateText = Localize.translateLocal('workspace.common.workspace'); + } + option.selected = participant.selected; + option.isSelected = participant.selected; + return option; +} + /** * Get the option for a policy expense report. */ @@ -2068,6 +2099,7 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + getReportOption, }; export type {MemberForList, CategorySection, GetOptions}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 67991d71a559..5ac7ae562de3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -921,6 +921,15 @@ function isConciergeChatReport(report: OnyxEntry): boolean { return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } +function findSelfDMReportID(): string | undefined { + if (!allReports) { + return; + } + + const selfDMReport = Object.values(allReports).find((report) => isSelfDM(report) && !isThread(report)); + return selfDMReport?.reportID; +} + /** * Checks if the supplied report belongs to workspace based on the provided params. If the report's policyID is _FAKE_ or has no value, it means this report is a DM. * In this case report and workspace members must be compared to determine whether the report belongs to the workspace. @@ -4341,7 +4350,7 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o /** * Helper method to define what money request options we want to show for particular method. - * There are 3 money request options: Request, Split and Send: + * There are 4 money request options: Request, Split, Send and Track expense: * - Request option should show for: * - DMs * - own policy expense chats @@ -4353,13 +4362,16 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o * - chat/ policy rooms with more than 1 participants * - groups chats with 3 and more participants * - corporate workspace chats + * - Track expense option should show for: + * - Self DMs + * - admin rooms * * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. */ -function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[]): Array> { +function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], canUseTrackExpense = true): Array> { // In any thread or task report, we do not allow any new money requests yet - if (isChatThread(report) || isTaskReport(report) || isSelfDM(report)) { + if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report))) { return []; } @@ -4387,6 +4399,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; - const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) - ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: true, selected: true}] - : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + const shouldAddAsReport = iouType === CONST.IOU.TYPE.TRACK_EXPENSE && !isEmptyObject(chatReport) && (ReportUtils.isSelfDM(chatReport) || ReportUtils.isAdminRoom(chatReport)); + const participants: Participant[] = + ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport + ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] + : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 68c7f0883683..e1e9fe25efda 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -13,6 +13,7 @@ import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -115,6 +116,7 @@ function AttachmentPickerWithMenuItems({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowHeight} = useWindowDimensions(); + const {canUseTrackExpense} = usePermissions(); /** * Returns the list of IOU Options @@ -136,12 +138,17 @@ function AttachmentPickerWithMenuItems({ text: translate('iou.sendMoney'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID), }, + [CONST.IOU.TYPE.TRACK_EXPENSE]: { + icon: Expensicons.TrackExpense, + text: translate('iou.trackExpense'), + onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), + }, }; - return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({ + return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ ...options[option], })); - }, [report, policy, reportParticipantIDs, translate]); + }, [translate, report, policy, reportParticipantIDs, canUseTrackExpense]); /** * Determines if we can show the task option diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index bcf9c77ac2f7..c075b8a84b89 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -192,14 +192,14 @@ function FloatingActionButtonAndPopover(props) { ? [ { icon: Expensicons.TrackExpense, - text: 'Track Expense', + text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest_temporaryForRefactor( CONST.IOU.TYPE.TRACK_EXPENSE, // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. - props.account.selfDMReportID || ReportUtils.generateReportID(), + props.account.selfDMReportID || ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), ), }, diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 1cd34db66da5..f0557d48da75 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -77,7 +77,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), - [CONST.IOU.TYPE.TRACK_EXPENSE]: 'Track Expense', + [CONST.IOU.TYPE.TRACK_EXPENSE]: translate('iou.trackExpense'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 9fdd2bea24f4..07882e95a9ae 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -144,7 +144,7 @@ function IOURequestStepAmount({ // inside a report. 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 (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index de5c6811d277..6a2a5dc6f70f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -101,15 +101,15 @@ function IOURequestStepConfirmation({ return translate('iou.splitBill'); } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - return 'Track Expense'; + return translate('iou.trackExpense'); } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + const participantReportID = lodashGet(participant, 'reportID', ''); + return participantReportID ? OptionsListUtils.getReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), [transaction.participants, personalDetails], ); @@ -130,7 +130,7 @@ function IOURequestStepConfirmation({ if (policyExpenseChat) { Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); } - }, [participants, transaction.billable, policy, transactionID]); + }, [isOffline, participants, transaction.billable, policy, transactionID]); const defaultBillable = lodashGet(policy, 'defaultBillable', false); useEffect(() => { @@ -186,13 +186,6 @@ function IOURequestStepConfirmation({ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); - useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); - } - }, [isOffline, participants, transaction.billable, policy]); - /** * @param {Array} selectedParticipants * @param {String} trimmedComment diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 320359192c8d..7df5df4cb203 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -127,7 +127,7 @@ function IOURequestStepDistance({ // inside a report. 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 (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7de121af52b4..05961bd6c4c3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -129,7 +129,7 @@ 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. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index f421417b53f6..2ef49af80441 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -189,7 +189,7 @@ 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. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index 29263d92078f..7a75e9f48805 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -131,6 +131,7 @@ function IOURequestStepTaxAmountPage({ // inside a report. 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 (report.reportID) { + // TODO: Is this really needed at all? IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; From 0873e42968a2ed50b410973afd4687d81e843bb9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 16:16:41 +0700 Subject: [PATCH 043/189] fix lint --- src/libs/actions/User.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e347fddfb4a7..77efb30ae874 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -492,9 +492,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { // "reportActions_5134363522480668" -> "5134363522480668" const reportIDs = reportActionsOnly .map((value) => value.key.split('_')[1]) - .filter((reportID) => { - return reportID === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus(); - }); + .filter((reportID) => reportID === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus()); Promise.all(reportIDs.map((reportID) => isChannelMuted(reportID))) .then((muted) => muted.every((isMuted) => isMuted)) From c2ef07b39e38d3da0b742db6ce65444164742fd7 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 20:30:56 +0530 Subject: [PATCH 044/189] Completed BE endpoint connection --- src/libs/API/parameters/TrackExpenseParams.ts | 25 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/IOU.ts | 343 ++++++++++++++++++ .../step/IOURequestStepConfirmation.js | 41 ++- 5 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/TrackExpenseParams.ts diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts new file mode 100644 index 000000000000..0e17a316bb9f --- /dev/null +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -0,0 +1,25 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {Receipt} from '@src/types/onyx/Transaction'; + +type TrackExpenseParams = { + amount: number; + currency: string; + comment: string; + created: string; + merchant: string; + iouReportID?: string; + chatReportID: string; + transactionID: string; + reportActionID: string; + createdChatReportActionID: string; + createdExpenseReportActionID?: string; + reportPreviewReportActionID?: string; + receipt: Receipt; + receiptState?: ValueOf; + tag?: string; + transactionThreadReportID: string; + createdReportActionIDForThread: string; +}; + +export default TrackExpenseParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 211bc06f26a3..d05dde006973 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -156,3 +156,4 @@ export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWor export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; +export type {default as TrackExpenseParams} from './TrackExpenseParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 115355210f75..9b47d1efd41d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -157,6 +157,7 @@ const WRITE_COMMANDS = { CANCEL_PAYMENT: 'CancelPayment', ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', + TRACK_EXPENSE: 'TrackExpense', } as const; type WriteCommand = ValueOf; @@ -312,6 +313,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; + [WRITE_COMMANDS.TRACK_EXPENSE]: Parameters.TrackExpenseParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bd1f92ab6490..26e78ee4cca8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -22,6 +22,7 @@ import type { SplitBillParams, StartSplitBillParams, SubmitReportParams, + TrackExpenseParams, UpdateMoneyRequestParams, } from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -84,6 +85,19 @@ type MoneyRequestInformation = { onyxData: OnyxData; }; +type TrackExpenseInformation = { + iouReport?: OnyxTypes.Report; + chatReport: OnyxTypes.Report; + transaction: OnyxTypes.Transaction; + iouAction: OptimisticIOUReportAction; + createdChatReportActionID: string; + createdExpenseReportActionID: string; + reportPreviewAction?: OnyxTypes.ReportAction; + transactionThreadReportID: string; + createdReportActionIDForThread: string; + onyxData: OnyxData; +}; + type SplitData = { chatReportID: string; transactionID: string; @@ -794,6 +808,159 @@ function buildOnyxDataForMoneyRequest( return [optimisticData, successData, failureData]; } +/** Builds the Onyx data for track expense */ +function buildOnyxDataForTrackExpense( + chatReport: OnyxEntry, + transaction: OnyxTypes.Transaction, + iouAction: OptimisticIOUReportAction, + transactionThreadReport: OptimisticChatReport, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction, +): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { + const isScanRequest = TransactionUtils.isScanRequest(transaction); + const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); + const optimisticData: OnyxUpdate[] = []; + + if (chatReport) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + lastMessageText: iouAction.message?.[0].text, + lastMessageHtml: iouAction.message?.[0].html, + lastReadTime: DateUtils.getDBTime(), + }, + }); + } + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: transaction, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: transactionThreadReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + }, + }, + + // Remove the temporary transaction used during the creation flow + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, + value: null, + }, + ); + + const successData: OnyxUpdate[] = []; + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + ); + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + lastReadTime: chatReport?.lastReadTime, + lastMessageText: chatReport?.lastMessageText, + lastMessageHtml: chatReport?.lastMessageHtml, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + }, + }, + }, + ]; + + return [optimisticData, successData, failureData]; +} + /** * Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead @@ -1017,6 +1184,125 @@ function getMoneyRequestInformation( }; } +/** + * Gathers all the data needed to make an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then + * it creates optimistic versions of them and uses those instead + */ +function getTrackExpenseInformation( + parentChatReport: OnyxEntry | EmptyObject, + participant: Participant, + comment: string, + amount: number, + currency: string, + created: string, + merchant: string, + receipt: Receipt | undefined, + payeeEmail = currentUserEmail, +): TrackExpenseInformation | EmptyObject { + // STEP 1: Get existing chat report + let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; + + // The chatReport always exist and we can get it from Onyx if chatReport is null. + if (!chatReport) { + chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null; + } + + // If we still don't have a report, it likely doens't exist and we will early return here as it should not happen + // Maybe later, we can build an optimistic selfDM chat. + if (!chatReport) { + return {}; + } + + // STEP 2: Get the money request report. + // TODO: This is deferred to later as we are not sure if we create iouReport at all in future. + // We can build an optimistic iouReport here if needed. + + // STEP 3: Build optimistic receipt and transaction + const receiptObject: Receipt = {}; + let filename; + if (receipt?.source) { + receiptObject.source = receipt.source; + receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; + filename = receipt.name; + } + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; + let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( + amount, + currency, + chatReport.reportID, + comment, + created, + '', + '', + merchant, + receiptObject, + filename, + null, + '', + '', + false, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + ); + + // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction + // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction + // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. + // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 + // to remind me to do this. + if (isDistanceRequest) { + optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false); + } + + // STEP 4: Build optimistic reportActions. We need: + // 1. IOU action for the chatReport + // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread + const currentTime = DateUtils.getDBTime(); + const iouAction = ReportUtils.buildOptimisticIOUReportAction( + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount, + currency, + comment, + [participant], + optimisticTransaction.transactionID, + undefined, + chatReport.reportID, + false, + false, + receiptObject, + false, + currentTime, + ); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); + + // STEP 5: Build Onyx Data + const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense( + chatReport, + optimisticTransaction, + iouAction, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, + ); + + return { + chatReport, + iouReport: undefined, + transaction: optimisticTransaction, + iouAction, + createdChatReportActionID: '0', + createdExpenseReportActionID: '0', + reportPreviewAction: undefined, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + onyxData: { + optimisticData, + successData, + failureData, + }, + }; +} + /** Requests money based on a distance (eg. mileage from a map) */ function createDistanceRequest( report: OnyxTypes.Report, @@ -1635,6 +1921,62 @@ function requestMoney( Report.notifyNewAction(activeReportID, payeeAccountID); } +/** + * Track an expense + */ +function trackExpense( + report: OnyxTypes.Report, + amount: number, + currency: string, + created: string, + merchant: string, + payeeEmail: string, + payeeAccountID: number, + participant: Participant, + comment: string, + receipt: Receipt, +) { + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); + const { + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdExpenseReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + onyxData, + } = getTrackExpenseInformation(report, participant, comment, amount, currency, currentCreated, merchant, receipt, payeeEmail); + const activeReportID = report.reportID; + + const parameters: TrackExpenseParams = { + amount, + currency, + comment, + created: currentCreated, + merchant, + iouReportID: iouReport?.reportID ?? '0', + chatReportID: chatReport.reportID, + transactionID: transaction.transactionID, + reportActionID: iouAction.reportActionID, + createdChatReportActionID, + createdExpenseReportActionID, + reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '0', + receipt, + receiptState: receipt?.state, + tag: '', + transactionThreadReportID, + createdReportActionIDForThread, + }; + + API.write(WRITE_COMMANDS.TRACK_EXPENSE, parameters, onyxData); + resetMoneyRequestInfo(); + Navigation.dismissModal(activeReportID); + Report.notifyNewAction(activeReportID, payeeAccountID); +} + /** * Build the Onyx data and IOU split necessary for splitting a bill with 3+ users. * 1. Build the optimistic Onyx data for the group chat, i.e. chatReport and iouReportAction creating the former if it doesn't yet exist. @@ -4347,4 +4689,5 @@ export { cancelPayment, navigateToStartStepIfScanFileCannotBeRead, savePreferredPaymentMethod, + trackExpense, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6a2a5dc6f70f..e518a2ac4616 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -218,6 +218,29 @@ function IOURequestStepConfirmation({ [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], ); + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + * @param {File} [receiptObj] + */ + const trackExpense = useCallback( + (selectedParticipants, trimmedComment, receiptObj) => { + IOU.trackExpense( + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + selectedParticipants[0], + trimmedComment, + receiptObj, + ); + }, + [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], + ); + /** * @param {Array} selectedParticipants * @param {String} trimmedComment @@ -309,6 +332,11 @@ function IOURequestStepConfirmation({ return; } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } + if (receiptFile) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { @@ -347,7 +375,18 @@ function IOURequestStepConfirmation({ requestMoney(selectedParticipants, trimmedComment); }, - [iouType, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, requestType, createDistanceRequest, requestMoney, receiptFile], + [ + transaction, + iouType, + receiptFile, + requestType, + requestMoney, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + report.reportID, + trackExpense, + createDistanceRequest, + ], ); /** From 17377dae19c912f0932ed6fed7e42fad63c747aa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:28:47 +0100 Subject: [PATCH 045/189] add type PolicyFeatureName --- src/types/onyx/Policy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index b005a9d2756f..c378c0eb3983 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -82,6 +82,8 @@ type Connection = { type AutoReportingOffset = number | ValueOf; +type PolicyFeatureName = 'areCategoriesEnabled' | 'areTagsEnabled' | 'areDistanceRatesEnabled' | 'areWorkflowsEnabled' | 'areReportFieldsEnabled' | 'areConnectionsEnabled' | 'tax'; + type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { /** The ID of the policy */ @@ -250,4 +252,4 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< export default Policy; -export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault}; +export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault, PolicyFeatureName}; From 7ed9fa60385e8802367147dcda6eee703c6a395d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:28:56 +0100 Subject: [PATCH 046/189] add a helper isPolicyFeatureEnabled --- src/libs/PolicyUtils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b510edd7dcf4..3dfbc8688297 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Navigation from './Navigation/Navigation'; @@ -268,6 +269,10 @@ function goBackFromInvalidPolicy() { Navigation.navigateWithSwitchPolicyID({route: ROUTES.ALL_SETTINGS}); } +function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, featureName: PolicyFeatureName): boolean { + return Boolean(policy?.[featureName]); +} + export { getActivePolicies, hasAccountingConnections, @@ -299,6 +304,7 @@ export { getPathWithoutPolicyID, getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, + isPolicyFeatureEnabled, }; export type {MemberEmailsToAccountIDs}; From c8a73121879bfdf872ba305f5bfe685a86678c93 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:29:06 +0100 Subject: [PATCH 047/189] implement FeatureEnabledAccessOrNotFoundComponent --- .../FeatureEnabledAccessOrNotFoundWrapper.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx new file mode 100644 index 000000000000..8f9ff546b98e --- /dev/null +++ b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx @@ -0,0 +1,70 @@ +/* eslint-disable rulesdir/no-negated-variables */ +import React, {useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import * as Policy from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type FeatureEnabledAccessOrNotFoundOnyxProps = { + /** The report currently being looked at */ + policy: OnyxEntry; + + /** Indicated whether the report data is loading */ + isLoadingReportData: OnyxEntry; +}; + +type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & { + /** The children to render */ + children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; + + /** The report currently being looked at */ + policyID: string; + + /** The current feature name that the user tries to get access */ + featureName: PolicyFeatureName; +}; + +function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) { + const isPolicyIDInRoute = !!props.policyID?.length; + + useEffect(() => { + if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { + // If the workspace is not required or is already loaded, we don't need to call the API + return; + } + + Policy.openWorkspace(props.policyID, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPolicyIDInRoute, props.policyID]); + + const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + + const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); + + if (shouldShowFullScreenLoadingIndicator) { + return ; + } + + if (shouldShowNotFoundPage) { + return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; + } + + return typeof props.children === 'function' ? props.children(props) : props.children; +} + +export default withOnyx({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, + }, + isLoadingReportData: { + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + }, +})(FeatureEnabledAccessOrNotFoundComponent); From cf310c3cddc0d9de1574f166cee92490dff039d8 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:29:41 +0100 Subject: [PATCH 048/189] integrate FeatureEnabledAccessOrNotFoundComponent to WorkspaceCategoriesPage --- .../categories/WorkspaceCategoriesPage.tsx | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 52d18d8de276..f2987bf624a1 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -23,6 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -152,51 +153,56 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat return ( - - - {!isSmallScreenWidth && headerButtons} - - {isSmallScreenWidth && {headerButtons}} - - {translate('workspace.categories.subtitle')} - - {isLoading && ( - - )} - {categoryList.length === 0 && !isLoading && ( - - )} - {categoryList.length > 0 && ( - - )} - + + {!isSmallScreenWidth && headerButtons} + + {isSmallScreenWidth && {headerButtons}} + + {translate('workspace.categories.subtitle')} + + {isLoading && ( + + )} + {categoryList.length === 0 && !isLoading && ( + + )} + {categoryList.length > 0 && ( + + )} + + ); From 941e7fa6390f7b81ac645bdf171f5780eb7be018 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:41:47 +0100 Subject: [PATCH 049/189] integrate more features consts --- src/CONST.ts | 9 +++++++++ .../workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- src/types/onyx/Policy.ts | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 6861fe174ffe..b0470046f9b6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1412,6 +1412,15 @@ const CONST = { MAKE_MEMBER: 'makeMember', MAKE_ADMIN: 'makeAdmin', }, + MORE_FEATURES: { + ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled', + ARE_TAGS_ENABLED: 'areTagsEnabled', + ARE_DISTANCE_RATES_ENABLED: 'areDistanceRatesEnabled', + ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled', + ARE_REPORTFIELDS_ENABLED: 'areReportFieldsEnabled', + ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled', + ARE_TAXES_ENABLED: 'tax', + }, }, CUSTOM_UNITS: { diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index f2987bf624a1..e3362d26f014 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -155,7 +155,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat ; -type PolicyFeatureName = 'areCategoriesEnabled' | 'areTagsEnabled' | 'areDistanceRatesEnabled' | 'areWorkflowsEnabled' | 'areReportFieldsEnabled' | 'areConnectionsEnabled' | 'tax'; +type PolicyFeatureName = ValueOf; type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { From 699f38a993dbbea3d80c8cce7780e9ed8be70996 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:11 +0100 Subject: [PATCH 050/189] integrate FeatureEnabledAccessOrNotFoundComponent to WorkspaceTagsPage --- .../workspace/tags/WorkspaceTagsPage.tsx | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index c82740eff361..125695856e4c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -19,7 +19,9 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -96,40 +98,45 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { return ( - - - - {translate('workspace.tags.subtitle')} - - {tagList.length ? ( - {}} - onSelectAll={toggleAllTags} - showScrollIndicator - ListItem={TableListItem} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + + - ) : ( - - )} - + + {translate('workspace.tags.subtitle')} + + {tagList.length ? ( + {}} + onSelectAll={toggleAllTags} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + /> + ) : ( + + )} + + ); From daacbfebcf1322ffedb65b430d6ea8e7c5ec4db9 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:18 +0100 Subject: [PATCH 051/189] integrate FeatureEnabledAccessOrNotFoundComponent to WorkspaceCategoriesSettingsPage --- .../WorkspaceCategoriesSettingsPage.tsx | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index a2ce06270c33..02ae87ce05d0 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; type WorkspaceCategoriesSettingsPageProps = StackScreenProps; @@ -27,33 +29,38 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag return ( - {({policy}) => ( - - - - - - - {translate('workspace.categories.requiresCategory')} - + + {({policy}) => ( + + + + + + + {translate('workspace.categories.requiresCategory')} + + - - - - - )} + + + + )} + ); From 7fb586ef9a7d7b255493c8d514a82024eac349fb Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:26 +0100 Subject: [PATCH 052/189] integrate FeatureEnabledAccessOrNotFoundComponent to CreateCategoryPage --- .../categories/CreateCategoryPage.tsx | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index cfe28ba292b0..332371c866b6 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -16,6 +16,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -68,34 +69,39 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) return ( - - - - - - + + + + + ); From 4bd3e8ac55a6140e96163deabeac37fc6bcfc0d5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:33 +0100 Subject: [PATCH 053/189] integrate FeatureEnabledAccessOrNotFoundComponent to CategorySettingsPage --- .../categories/CategorySettingsPage.tsx | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 16f128e5ea1f..97e71abb7a2e 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -16,8 +16,10 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -46,36 +48,41 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro return ( - - - - Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} - > - - - {translate('workspace.categories.enableCategory')} - + + + + Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} + > + + + {translate('workspace.categories.enableCategory')} + + - - - - - + + + + + ); From a40eef3c48c5bc0eb2bfa997a136e4a724be154c Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 8 Mar 2024 03:10:14 +0500 Subject: [PATCH 054/189] fix: ts errors --- src/components/ReportActionItem/MoneyReportView.tsx | 2 +- src/libs/ReportUtils.ts | 3 +-- src/types/onyx/Policy.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index f0d5c4d125e4..7e4b1d9187b4 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -57,7 +57,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport ]; const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy.reportFields || {})); + const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.reportFields ?? {})); return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bcb373ab2716..053a99ad9afe 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -23,7 +23,6 @@ import type { PersonalDetailsList, Policy, PolicyReportField, - PolicyReportFields, Report, ReportAction, ReportMetadata, @@ -2042,7 +2041,7 @@ function getFormulaTypeReportField(reportFields: Record) { return Object.values(reportFields).find((field) => isReportFieldOfTypeTitle(field)); } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index db99aab0167a..12b6fd5e18cc 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -281,4 +281,4 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< export default Policy; -export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault}; +export type {PolicyReportField, PolicyReportFieldType, Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault}; From 2a425ad50f68f544c5cf513c7b1176f48904dc00 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 8 Mar 2024 17:22:35 +0530 Subject: [PATCH 055/189] Added gpsPoints in endpoint --- src/libs/API/parameters/TrackExpenseParams.ts | 1 + src/libs/actions/IOU.ts | 11 ++++--- .../step/IOURequestStepConfirmation.js | 33 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 0e17a316bb9f..9965463235cc 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -18,6 +18,7 @@ type TrackExpenseParams = { receipt: Receipt; receiptState?: ValueOf; tag?: string; + gpsPoints?: string; transactionThreadReportID: string; createdReportActionIDForThread: string; }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 26e78ee4cca8..8342d2d466f8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -91,7 +91,7 @@ type TrackExpenseInformation = { transaction: OnyxTypes.Transaction; iouAction: OptimisticIOUReportAction; createdChatReportActionID: string; - createdExpenseReportActionID: string; + createdExpenseReportActionID?: string; reportPreviewAction?: OnyxTypes.ReportAction; transactionThreadReportID: string; createdReportActionIDForThread: string; @@ -1291,7 +1291,7 @@ function getTrackExpenseInformation( transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdExpenseReportActionID: '0', + createdExpenseReportActionID: undefined, reportPreviewAction: undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, @@ -1935,6 +1935,7 @@ function trackExpense( participant: Participant, comment: string, receipt: Receipt, + gpsPoints = undefined, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { @@ -1957,16 +1958,18 @@ function trackExpense( comment, created: currentCreated, merchant, - iouReportID: iouReport?.reportID ?? '0', + iouReportID: iouReport?.reportID, chatReportID: chatReport.reportID, transactionID: transaction.transactionID, reportActionID: iouAction.reportActionID, createdChatReportActionID, createdExpenseReportActionID, - reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '0', + reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, receiptState: receipt?.state, tag: '', + // This needs to be a string of JSON because of limitations with the fetch() API and nested objects + gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, transactionThreadReportID, createdReportActionIDForThread, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index e518a2ac4616..6285fd1c4e23 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -224,7 +224,7 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const trackExpense = useCallback( - (selectedParticipants, trimmedComment, receiptObj) => { + (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { IOU.trackExpense( report, transaction.amount, @@ -236,6 +236,7 @@ function IOURequestStepConfirmation({ selectedParticipants[0], trimmedComment, receiptObj, + gpsPoints, ); }, [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], @@ -333,6 +334,36 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + if (receiptFile) { + // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. + if (transaction.amount === 0) { + getCurrentPosition( + (successData) => { + trackExpense(selectedParticipants, trimmedComment, receiptFile, { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }); + }, + (errorData) => { + Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); + // When there is an error, the money can still be requested, it just won't include the GPS coordinates + trackExpense(selectedParticipants, trimmedComment, receiptFile); + }, + { + // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in + maximumAge: 1000 * 60 * 60, + + // 15 seconds, don't wait too long because the server can always fall back to using the IP address + timeout: 15000, + }, + ); + return; + } + + // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } trackExpense(selectedParticipants, trimmedComment, receiptFile); return; } From 87fcc3316bdc827766874ff90d4e9bfa298f3a69 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sat, 9 Mar 2024 02:13:25 +0500 Subject: [PATCH 056/189] rename report fields as field list --- .../ReportActionItem/MoneyReportView.tsx | 2 +- src/libs/ReportUtils.ts | 8 +++--- src/libs/actions/Report.ts | 6 ++-- src/pages/EditReportFieldPage.tsx | 4 +-- src/pages/home/ReportScreen.js | 4 +-- src/pages/home/report/ReportActionItem.tsx | 2 +- src/types/onyx/Policy.ts | 28 +++++++++++++++++-- src/types/onyx/Report.ts | 2 +- 8 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 7e4b1d9187b4..61ff493e5358 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -57,7 +57,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport ]; const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.reportFields ?? {})); + const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {})); return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 84361636acb4..6967727bd872 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2063,7 +2063,7 @@ function getTitleReportField(reportFields: Record) { * Get the report fields attached to the policy given policyID */ function getReportFieldsByPolicyID(policyID: string) { - return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.reportFields; + return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.fieldList; } /** @@ -2072,7 +2072,7 @@ function getReportFieldsByPolicyID(policyID: string) { function getAvailableReportFields(report: Report, policyReportFields: PolicyReportField[]): PolicyReportField[] { // Get the report fields that are attached to a report. These will persist even if a field is deleted from the policy. - const reportFields = Object.values(report.reportFields ?? {}); + const reportFields = Object.values(report.fieldList ?? {}); const reportIsSettled = isSettled(report.reportID); // If the report is settled, we don't want to show any new field that gets added to the policy. @@ -2083,7 +2083,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo // If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that // are attached to the report. const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); - return mergedFieldIds.map((id) => report?.reportFields?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[]; + return mergedFieldIds.map((id) => report?.fieldList?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[]; } /** @@ -2091,7 +2091,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo */ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { const isReportSettled = isSettled(report?.reportID ?? ''); - const reportFields = isReportSettled ? report?.reportFields : getReportFieldsByPolicyID(report?.policyID ?? ''); + const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? ''); const titleReportField = getFormulaTypeReportField(reportFields ?? {}); if (titleReportField && report?.reportName && reportFieldsEnabled(report)) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 94fe324d306a..c266b4d43887 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1617,7 +1617,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportFields: { + fieldList: { [reportField.fieldID]: reportField, }, pendingFields: { @@ -1627,7 +1627,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre }, ]; - if (reportField.type === 'dropdown') { + if (reportField.type === 'dropdown' && reportField.value) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, @@ -1642,7 +1642,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportFields: { + fieldList: { [reportField.fieldID]: previousReportField, }, pendingFields: { diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 015b2cabd51c..95620a2b9389 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -40,7 +40,7 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { - const reportField = report?.reportFields?.[route.params.fieldID] ?? policy?.reportFields?.[route.params.fieldID]; + const reportField = report?.fieldList?.[route.params.fieldID] ?? policy?.fieldList?.[route.params.fieldID]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { @@ -105,7 +105,7 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) fieldID={reportField.fieldID} fieldName={Str.UCFirst(reportField.name)} fieldValue={fieldValue} - fieldOptions={reportField.values} + fieldOptions={reportField.values.filter((value) => !(value in reportField.disabledOptions))} onSubmit={handleReportFieldChange} /> ); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index da5a8e4aae27..689ac8b7d962 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -192,7 +192,7 @@ function ReportScreen({ managerID: reportProp.managerID, total: reportProp.total, nonReimbursableTotal: reportProp.nonReimbursableTotal, - reportFields: reportProp.reportFields, + reportFields: reportProp.fieldList, ownerAccountID: reportProp.ownerAccountID, currency: reportProp.currency, participantAccountIDs: reportProp.participantAccountIDs, @@ -229,7 +229,7 @@ function ReportScreen({ reportProp.managerID, reportProp.total, reportProp.nonReimbursableTotal, - reportProp.reportFields, + reportProp.fieldList, reportProp.ownerAccountID, reportProp.currency, reportProp.participantAccountIDs, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index e8cf1cf23af9..6306c89da40c 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -921,7 +921,7 @@ export default withOnyx({ prevProps.report?.total === nextProps.report?.total && prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) && + lodashIsEqual(prevProps.report.fieldList, nextProps.report.fieldList) && lodashIsEqual(prevProps.policy, nextProps.policy) && lodashIsEqual(prevParentReportAction, nextParentReportAction) ); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index c45a5ad32c2d..65ce91544541 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -104,10 +104,32 @@ type PolicyReportField = { deletable: boolean; /** Value of the field */ - value: string; + value: string | null; /** Options to select from if field is of type dropdown */ values: string[]; + + target: string; + + /** Tax UDFs have keys holding the names of taxes (eg, VAT), values holding percentages (eg, 15%) and a value indicating the currently selected tax value (eg, 15%). */ + keys: string[]; + + /** list of externalIDs, this are either imported from the integrations or auto generated by us, each externalID */ + externalIDs: string[]; + + disabledOptions: boolean[]; + + /** Is this a tax user defined report field */ + isTax: boolean; + + /** This is the selected externalID in an expense. */ + externalID?: string | null; + + /** Automated action or integration that added this report field */ + origin?: string | null; + + /** This is indicates which default value we should use. It was preferred using this over having defaultValue (which we have anyway for historical reasons), since the values are not unique we can't determine which key the defaultValue is referring too. It was also preferred over having defaultKey since the keys are user editable and can be changed. The externalIDs work effectively as an ID, which never changes even after changing the key, value or position of the option. */ + defaultExternalID?: string | null; }; type PendingJoinRequestPolicy = { @@ -122,7 +144,7 @@ type PendingJoinRequestPolicy = { avatar?: string; }> >; -} +}; type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { @@ -270,7 +292,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< connections?: Record; /** Report fields attached to the policy */ - reportFields?: Record; + fieldList?: Record; /** Whether the Categories feature is enabled */ areCategoriesEnabled?: boolean; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 49e5b07e9181..7c2570314243 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -171,7 +171,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< selected?: boolean; /** If the report contains reportFields, save the field id and its value */ - reportFields?: Record; + fieldList?: Record; }, PolicyReportField['fieldID'] >; From 92f803ae9a0bcfcee4a84cf312e935a833656664 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 9 Mar 2024 18:30:21 +0530 Subject: [PATCH 057/189] Fixing on which room we should allow track expense --- src/libs/API/parameters/TrackExpenseParams.ts | 6 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU.ts | 78 ++++++++++++++++--- .../iou/request/step/IOURequestStepAmount.js | 2 +- .../step/IOURequestStepConfirmation.js | 26 ++++++- .../request/step/IOURequestStepDistance.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- 8 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 9965463235cc..f48c8666f109 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -13,11 +13,15 @@ type TrackExpenseParams = { transactionID: string; reportActionID: string; createdChatReportActionID: string; - createdExpenseReportActionID?: string; + createdIOUReportActionID?: string; reportPreviewReportActionID?: string; receipt: Receipt; receiptState?: ValueOf; + category?: string; tag?: string; + taxCode: string; + taxAmount: number; + billable?: boolean; gpsPoints?: string; transactionThreadReportID: string; createdReportActionIDForThread: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 45d95a6f47be..0f5cc3507ed4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4498,7 +4498,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const isScanRequest = TransactionUtils.isScanRequest(transaction); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); @@ -958,6 +961,22 @@ function buildOnyxDataForTrackExpense( }, ]; + // We don't need to compute violations unless we're on a paid policy + if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { + return [optimisticData, successData, failureData]; + } + + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + + if (violationsOnyxData) { + optimisticData.push(violationsOnyxData); + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: [], + }); + } + return [optimisticData, successData, failureData]; } @@ -1186,6 +1205,12 @@ function getTrackExpenseInformation( created: string, merchant: string, receipt: Receipt | undefined, + category: string | undefined, + tag: string | undefined, + billable: boolean | undefined, + policy: OnyxEntry | undefined, + policyTagList: OnyxEntry | undefined, + policyCategories: OnyxEntry | undefined, payeeEmail = currentUserEmail, ): TrackExpenseInformation | EmptyObject { // STEP 1: Get existing chat report @@ -1228,9 +1253,9 @@ function getTrackExpenseInformation( receiptObject, filename, null, - '', - '', - false, + category, + tag, + billable, isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, ); @@ -1272,6 +1297,9 @@ function getTrackExpenseInformation( iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread, + policy, + policyTagList, + policyCategories, ); return { @@ -1280,7 +1308,7 @@ function getTrackExpenseInformation( transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdExpenseReportActionID: undefined, + createdIOUReportActionID: undefined, reportPreviewAction: undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, @@ -1924,6 +1952,14 @@ function trackExpense( participant: Participant, comment: string, receipt: Receipt, + category?: string, + tag?: string, + taxCode = '', + taxAmount = 0, + billable?: boolean, + policy?: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, gpsPoints = undefined, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); @@ -1933,12 +1969,28 @@ function trackExpense( transaction, iouAction, createdChatReportActionID, - createdExpenseReportActionID, + createdIOUReportActionID, reportPreviewAction, transactionThreadReportID, createdReportActionIDForThread, onyxData, - } = getTrackExpenseInformation(report, participant, comment, amount, currency, currentCreated, merchant, receipt, payeeEmail); + } = getTrackExpenseInformation( + report, + participant, + comment, + amount, + currency, + currentCreated, + merchant, + receipt, + category, + tag, + billable, + policy, + policyTagList, + policyCategories, + payeeEmail, + ); const activeReportID = report.reportID; const parameters: TrackExpenseParams = { @@ -1952,11 +2004,15 @@ function trackExpense( transactionID: transaction.transactionID, reportActionID: iouAction.reportActionID, createdChatReportActionID, - createdExpenseReportActionID, + createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, receiptState: receipt?.state, - tag: '', + category, + tag, + taxCode, + taxAmount, + billable, // This needs to be a string of JSON because of limitations with the fetch() API and nested objects gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, transactionThreadReportID, @@ -4464,11 +4520,11 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param transactionID of the transaction to set the participants of * @param report attached to the transaction */ -function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report, iouType: ValueOf) { +function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; - const shouldAddAsReport = iouType === CONST.IOU.TYPE.TRACK_EXPENSE && !isEmptyObject(chatReport) && (ReportUtils.isSelfDM(chatReport) || ReportUtils.isAdminRoom(chatReport)); + const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 07882e95a9ae..9fdd2bea24f4 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -144,7 +144,7 @@ function IOURequestStepAmount({ // inside a report. 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 (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6285fd1c4e23..b7c669dcebc9 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -236,10 +236,34 @@ function IOURequestStepConfirmation({ selectedParticipants[0], trimmedComment, receiptObj, + transaction.category, + transaction.tag, + transactionTaxCode, + transactionTaxAmount, + transaction.billable, + policy, + policyTags, + policyCategories, gpsPoints, ); }, - [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], + [ + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + transaction.category, + transaction.tag, + transaction.billable, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transactionTaxCode, + transactionTaxAmount, + policy, + policyTags, + policyCategories, + ], ); /** diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 7df5df4cb203..320359192c8d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -127,7 +127,7 @@ function IOURequestStepDistance({ // inside a report. 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 (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 05961bd6c4c3..7de121af52b4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -129,7 +129,7 @@ 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. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index 2ef49af80441..f421417b53f6 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -189,7 +189,7 @@ 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. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); From 09203e49dbdf1bdf9ffc0155b4bc501255526796 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 11 Mar 2024 18:44:28 +0100 Subject: [PATCH 058/189] confirm navigation after onyx updates --- src/libs/actions/Policy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 8bfa2a4a11fd..6c5df9f94c95 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -2662,7 +2662,11 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { return; } - Navigation.navigate(featureRoute); + new Promise((resolve) => { + resolve(); + }).then(() => { + Navigation.navigate(featureRoute); + }); } function enablePolicyCategories(policyID: string, enabled: boolean) { From 2040059e6f5efb16660d30e182dd2a7c78b7c057 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 11 Mar 2024 18:44:48 +0100 Subject: [PATCH 059/189] redirect instead of not found --- ...FeatureEnabledAccessOrRedirectWrapper.tsx} | 30 ++++++++++--------- .../categories/CategorySettingsPage.tsx | 6 ++-- .../categories/CreateCategoryPage.tsx | 6 ++-- .../categories/WorkspaceCategoriesPage.tsx | 6 ++-- .../WorkspaceCategoriesSettingsPage.tsx | 6 ++-- .../workspace/tags/WorkspaceTagsPage.tsx | 6 ++-- 6 files changed, 31 insertions(+), 29 deletions(-) rename src/pages/workspace/{FeatureEnabledAccessOrNotFoundWrapper.tsx => FeatureEnabledAccessOrRedirectWrapper.tsx} (68%) diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx similarity index 68% rename from src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx rename to src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx index 8f9ff546b98e..d5799e617226 100644 --- a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx @@ -5,7 +5,6 @@ import {withOnyx} from 'react-native-onyx'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -13,7 +12,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -type FeatureEnabledAccessOrNotFoundOnyxProps = { +type FeatureEnabledAccessOrRedirectOnyxProps = { /** The report currently being looked at */ policy: OnyxEntry; @@ -21,9 +20,9 @@ type FeatureEnabledAccessOrNotFoundOnyxProps = { isLoadingReportData: OnyxEntry; }; -type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & { +type FeatureEnabledAccessOrRedirectComponentProps = FeatureEnabledAccessOrRedirectOnyxProps & { /** The children to render */ - children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; + children: ((props: FeatureEnabledAccessOrRedirectOnyxProps) => React.ReactNode) | React.ReactNode; /** The report currently being looked at */ policyID: string; @@ -32,8 +31,10 @@ type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFou featureName: PolicyFeatureName; }; -function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) { +function FeatureEnabledAccessOrRedirectComponent(props: FeatureEnabledAccessOrRedirectComponentProps) { const isPolicyIDInRoute = !!props.policyID?.length; + const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + const shouldRedirect = !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { @@ -45,26 +46,27 @@ function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNo // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPolicyIDInRoute, props.policyID]); - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + useEffect(() => { + if (!shouldRedirect) { + return; + } - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(props.policyID)); + }, [props.policyID, shouldRedirect]); - if (shouldShowFullScreenLoadingIndicator) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (shouldShowFullScreenLoadingIndicator || shouldRedirect) { return ; } - if (shouldShowNotFoundPage) { - return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; - } - return typeof props.children === 'function' ? props.children(props) : props.children; } -export default withOnyx({ +export default withOnyx({ policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, -})(FeatureEnabledAccessOrNotFoundComponent); +})(FeatureEnabledAccessOrRedirectComponent); diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 97e71abb7a2e..cfb55ead8d6b 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -16,7 +16,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -48,7 +48,7 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro return ( - @@ -82,7 +82,7 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro /> - + ); diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index 332371c866b6..f36cbac03ae3 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -16,7 +16,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -69,7 +69,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) return ( - @@ -101,7 +101,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) /> - + ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index ce1d891d631e..397216686eea 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -23,7 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -154,7 +154,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat return ( - @@ -203,7 +203,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat /> )} - + ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 02ae87ce05d0..b0882573d51c 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; @@ -29,7 +29,7 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag return ( - @@ -60,7 +60,7 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag )} - + ); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index efb7ff1296c8..b68b0991f167 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -19,7 +19,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -98,7 +98,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { return ( - @@ -136,7 +136,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { /> )} - + ); From 68e55e554e1ce92c0b8c38d240ab38c16e832fed Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 12 Mar 2024 12:24:29 +0100 Subject: [PATCH 060/189] integrate FeatureEnabledAccessOrRedirectWrapper to PolicyDistanceRatesPage --- .../distanceRates/PolicyDistanceRatesPage.tsx | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index fd6466da1758..0134771b9ec5 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -22,6 +22,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; @@ -232,52 +233,57 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) return ( - - - {!isSmallScreenWidth && headerButtons} - - {isSmallScreenWidth && {headerButtons}} - - {translate('workspace.distanceRates.centrallyManage')} - - {isLoading && ( - - )} - {Object.values(customUnitRates).length > 0 && ( - + {!isSmallScreenWidth && headerButtons} + + {isSmallScreenWidth && {headerButtons}} + + {translate('workspace.distanceRates.centrallyManage')} + + {isLoading && ( + + )} + {Object.values(customUnitRates).length > 0 && ( + + )} + setIsWarningModalVisible(false)} + isVisible={isWarningModalVisible} + title={translate('workspace.distanceRates.oopsNotSoFast')} + prompt={translate('workspace.distanceRates.workspaceNeeds')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} /> - )} - setIsWarningModalVisible(false)} - isVisible={isWarningModalVisible} - title={translate('workspace.distanceRates.oopsNotSoFast')} - prompt={translate('workspace.distanceRates.workspaceNeeds')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} - /> - + + ); From 1c1d7fd71312612ca94d2cbc84934701680fc92c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 12 Mar 2024 12:24:37 +0100 Subject: [PATCH 061/189] integrate FeatureEnabledAccessOrRedirectWrapper to tags pages --- .../workspace/tags/WorkspaceEditTagsPage.tsx | 56 +++++++------ .../tags/WorkspaceTagsSettingsPage.tsx | 81 ++++++++++--------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index 98ae6f726d73..b011051999ae 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -16,6 +16,7 @@ import * as Policy from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -52,33 +53,38 @@ function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps) ); return ( - - - - - - - - + + + + + + + + ); } diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index deac804980ea..7d675eb7fec3 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -16,7 +16,9 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -43,44 +45,49 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag return ( - {({policy}) => ( - - - - - - - {translate('workspace.tags.requiresTag')} - + + {({policy}) => ( + + + + + + + {translate('workspace.tags.requiresTag')} + + - - - - Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} - /> - - - - )} + + + Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} + /> + + + + )} + ); From 45d502c521b3ab7303d6ec3cd0cb7fc0f4e943d6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 12 Mar 2024 12:24:44 +0100 Subject: [PATCH 062/189] integrate FeatureEnabledAccessOrRedirectWrapper to workflows pages --- .../WorkspaceAutoReportingFrequencyPage.tsx | 53 ++++++++------- ...orkspaceAutoReportingMonthlyOffsetPage.tsx | 63 ++++++++++-------- .../WorkspaceWorkflowsApproverPage.tsx | 65 +++++++++++-------- .../workflows/WorkspaceWorkflowsPage.tsx | 54 ++++++++------- 4 files changed, 135 insertions(+), 100 deletions(-) diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index cf66af726a72..5a7e7a2fc3a9 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} from 'react'; import {FlatList} from 'react-native-gesture-handler'; import type {ValueOf} from 'type-fest'; @@ -11,18 +12,21 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AutoReportingFrequencyKey = Exclude, 'instant'>; type Locale = ValueOf; -type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps; +type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & StackScreenProps; type WorkspaceAutoReportingFrequencyPageItem = { text: string; @@ -41,7 +45,7 @@ const getAutoReportingFrequencyDisplayNames = (locale: Locale): AutoReportingFre [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: Localize.translate(locale, 'workflowsPage.frequencies.manually'), }); -function WorkspaceAutoReportingFrequencyPage({policy}: WorkspaceAutoReportingFrequencyPageProps) { +function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoReportingFrequencyPageProps) { const {translate, preferredLocale, toLocaleOrdinal} = useLocalize(); const styles = useThemeStyles(); const [isMonthlyFrequency, setIsMonthlyFrequency] = useState(policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY); @@ -105,28 +109,33 @@ function WorkspaceAutoReportingFrequencyPage({policy}: WorkspaceAutoReportingFre ); return ( - - - - - item.text} - /> - - + + + + item.text} + /> + + + ); } diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 84d70e799c42..3aad5fb9f0a4 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} from 'react'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -7,16 +8,19 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const DAYS_OF_MONTH = 28; -type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps; +type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps & StackScreenProps; type AutoReportingOffsetKeys = ValueOf; @@ -27,7 +31,7 @@ type WorkspaceAutoReportingMonthlyOffsetPageItem = { isNumber?: boolean; }; -function WorkspaceAutoReportingMonthlyOffsetPage({policy}: WorkspaceAutoReportingMonthlyOffsetProps) { +function WorkspaceAutoReportingMonthlyOffsetPage({policy, route}: WorkspaceAutoReportingMonthlyOffsetProps) { const {translate, toLocaleOrdinal} = useLocalize(); const offset = policy?.autoReportingOffset ?? 0; const [searchText, setSearchText] = useState(''); @@ -67,34 +71,39 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy}: WorkspaceAutoReportin }; return ( - - - + + - - - + + + + ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index 52406a8033d2..34c0f8989888 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -17,15 +18,18 @@ import compose from '@libs/compose'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import type {PersonalDetailsList, PolicyMember} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -34,11 +38,13 @@ type WorkspaceWorkflowsApproverPageOnyxProps = { personalDetails: OnyxEntry; }; -type WorkspaceWorkflowsApproverPageProps = WorkspaceWorkflowsApproverPageOnyxProps & WithPolicyAndFullscreenLoadingProps; +type WorkspaceWorkflowsApproverPageProps = WorkspaceWorkflowsApproverPageOnyxProps & + WithPolicyAndFullscreenLoadingProps & + StackScreenProps; type MemberOption = Omit & {accountID: number}; type MembersSection = SectionListData>; -function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsApproverPageProps) { +function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApproverPageProps) { const {translate} = useLocalize(); const policyName = policy?.name ?? ''; const [searchTerm, setSearchTerm] = useState(''); @@ -161,33 +167,38 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, }; return ( - - - - - - + + + + + + ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index ee3934cacc06..706b947b2704 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -17,6 +17,7 @@ import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; @@ -167,31 +168,36 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); return ( - - -
- - {translate('workflowsPage.workflowDescription')} - item.title} - /> - -
-
-
+ + +
+ + {translate('workflowsPage.workflowDescription')} + item.title} + /> + +
+
+
+ ); } From 6aa7212618fe0844871c94f080e4e621c8ad862c Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 12 Mar 2024 14:50:42 +0200 Subject: [PATCH 063/189] Desktop - Login - Unable to enter the 2FA code or exit the screen --- src/libs/Navigation/Navigation.ts | 9 +++++++++ src/libs/desktopLoginRedirect/index.desktop.ts | 16 ++++++++++++++++ src/libs/desktopLoginRedirect/index.ts | 5 +++++ src/pages/ValidateLoginPage/index.website.tsx | 6 ++++++ 4 files changed, 36 insertions(+) create mode 100644 src/libs/desktopLoginRedirect/index.desktop.ts create mode 100644 src/libs/desktopLoginRedirect/index.ts diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 4cd6a141bd3b..e05084e18690 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -347,6 +347,14 @@ function navigateWithSwitchPolicyID(params: SwitchPolicyIDParams) { return switchPolicyID(navigationRef.current, params); } +/** + * The `popToTop` action takes you back to the first screen in the stack, dismissing all the others. + * @note we used to call `Navigation.navigate()` before the new navigation was introduced. + */ +function popToTop() { + navigationRef.current?.dispatch(StackActions.popToTop()); +} + export default { setShouldPopAllStateOnUP, navigate, @@ -366,6 +374,7 @@ export default { parseHybridAppUrl, closeFullScreen, navigateWithSwitchPolicyID, + popToTop, }; export {navigationRef}; diff --git a/src/libs/desktopLoginRedirect/index.desktop.ts b/src/libs/desktopLoginRedirect/index.desktop.ts new file mode 100644 index 000000000000..e751fa1ffd78 --- /dev/null +++ b/src/libs/desktopLoginRedirect/index.desktop.ts @@ -0,0 +1,16 @@ +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import type {AutoAuthState} from '@src/types/onyx/Session'; + +function desktopLoginRedirect(autoAuthState: AutoAuthState, isSignedIn: boolean) { + // NOT_STARTED - covers edge case of autoAuthState not being initialized yet (after logout) + // JUST_SIGNED_IN - confirms passing the magic code step -> we're either logged-in or shown 2FA screen + // !isSignedIn - confirms we're not signed-in yet as there's possible one last step (2FA validation) + const shouldPopToTop = (autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN) && !isSignedIn; + + if (shouldPopToTop) { + Navigation.isNavigationReady().then(() => Navigation.popToTop()); + } +} + +export default desktopLoginRedirect; diff --git a/src/libs/desktopLoginRedirect/index.ts b/src/libs/desktopLoginRedirect/index.ts new file mode 100644 index 000000000000..14f5750c3de9 --- /dev/null +++ b/src/libs/desktopLoginRedirect/index.ts @@ -0,0 +1,5 @@ +import type {AutoAuthState} from '@src/types/onyx/Session'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function desktopLoginRedirect(autoAuthState: AutoAuthState, isSignedIn: boolean) {} +export default desktopLoginRedirect; diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx index 2acad7815754..b8e8709215e8 100644 --- a/src/pages/ValidateLoginPage/index.website.tsx +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -4,6 +4,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import ExpiredValidateCodeModal from '@components/ValidateCode/ExpiredValidateCodeModal'; import JustSignedInModal from '@components/ValidateCode/JustSignedInModal'; import ValidateCodeModal from '@components/ValidateCode/ValidateCodeModal'; +import desktopLoginRedirect from '@libs/desktopLoginRedirect'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; @@ -43,6 +44,11 @@ function ValidateLoginPage({ // The user has initiated the sign in process on the same browser, in another tab. Session.signInWithValidateCode(Number(accountID), validateCode); + + // Since on Desktop we don't have multi-tab functionality to handle the login flow, + // we need to `popToTop` the stack after `signInWithValidateCode` in order to + // perform login for both 2FA and non-2FA accounts. + desktopLoginRedirect(autoAuthState, isSignedIn); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 8de01d0f2c7a81dfa8a542c071b93aa6e04714f5 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 12 Mar 2024 16:10:52 +0100 Subject: [PATCH 064/189] address comments --- src/types/onyx/ReportAction.ts | 6 +- tests/unit/MigrationTest.ts | 192 ++++++++++++++++++--------------- 2 files changed, 105 insertions(+), 93 deletions(-) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 4512f04964b8..f6c34fe742a4 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -2,8 +2,6 @@ import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; -import type ONYXKEYS from '@src/ONYXKEYS'; -import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as OnyxCommon from './OnyxCommon'; import type {Decision, Reaction} from './OriginalMessage'; @@ -229,7 +227,5 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; -type ReportActionCollectionDataSet = CollectionDataSet; - export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionCollectionDataSet}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage}; diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index d60761cd1d89..147588559e13 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -5,10 +5,11 @@ import Log from '@src/libs/Log'; import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActionCollectionDataSet} from '@src/types/onyx/ReportAction'; import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; +import { toCollectionDataSet } from '@src/types/utils/CollectionDataSet'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + jest.mock('@src/libs/getPlatform'); let LogSpy: jest.SpyInstance>; @@ -34,22 +35,23 @@ describe('Migrations', () => { )); it('Should remove all report actions given that a previousReportActionID does not exist', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { - 1: { - reportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '1', + }, + 2: {reportActionID: '2', created: '', actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, reportID: '1'}, + }, + ], + (item) => item[1].reportID ?? '', + ); - return Onyx.multiSet(setQueries) + return Onyx.multiSet(reportActionsCollectionDataSet) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -68,24 +70,30 @@ describe('Migrations', () => { }); it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { - 1: { - reportActionID: '1', - previousReportActionID: '0', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - previousReportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + previousReportActionID: '0', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '1', + }, + 2: { + reportActionID: '2', + previousReportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '1', + }, + }, + ], + (item) => item[1].reportID ?? '', + ); - return Onyx.multiSet(setQueries) + return Onyx.multiSet(reportActionsCollectionDataSet) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -111,29 +119,33 @@ describe('Migrations', () => { }); it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; - - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { - 1: { - reportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; - - return Onyx.multiSet(setQueries) + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + 2: { + reportActionID: '2', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + }, + ], + (item) => item[1].reportID ?? '', + ); + + return Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, + ...reportActionsCollectionDataSet, + }) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -155,30 +167,35 @@ describe('Migrations', () => { }); it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { - 1: { - reportActionID: '1', - previousReportActionID: '10', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - previousReportActionID: '23', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; - - return Onyx.multiSet(setQueries) + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + previousReportActionID: '10', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + 2: { + reportActionID: '2', + previousReportActionID: '23', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + }, + ], + (item) => item[1].reportID ?? '', + ); + + return Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, + ...reportActionsCollectionDataSet, + }) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -208,15 +225,14 @@ describe('Migrations', () => { }); it('Should skip if no valid reportActions', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = null; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = {}; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = {}; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = null; + const setQueries = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: null, + }; + // @ts-expect-error preset null values return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { From 8a1164a889288b268478c8ecad9bdbb82d18f203 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 13 Mar 2024 00:46:42 +0500 Subject: [PATCH 065/189] use prefix for report fields --- src/libs/ReportUtils.ts | 30 +++++++++++++++++++++++++++--- src/pages/EditReportFieldPage.tsx | 3 ++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 28ec6880c371..7713d7494eae 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2076,8 +2076,15 @@ function getTitleReportField(reportFields: Record) { /** * Get the report fields attached to the policy given policyID */ -function getReportFieldsByPolicyID(policyID: string) { - return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.fieldList; +function getReportFieldsByPolicyID(policyID: string): Record { + const policyReportFields = Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID); + const fieldList = policyReportFields?.[1]?.fieldList; + + if (!policyReportFields || !fieldList) { + return {}; + } + + return fieldList as Record; } /** @@ -2097,7 +2104,24 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo // If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that // are attached to the report. const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); - return mergedFieldIds.map((id) => report?.fieldList?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[]; + + const fields = mergedFieldIds.map((id) => { + const field = report?.fieldList?.[`expensify_${id as string}`]; + + if (field) { + return field as PolicyReportField; + } + + const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); + + if (policyReportField) { + return policyReportField; + } + + return null; + }); + + return fields.filter(Boolean) as PolicyReportField[]; } /** diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 95620a2b9389..6f9886af4482 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -40,7 +40,8 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { - const reportField = report?.fieldList?.[route.params.fieldID] ?? policy?.fieldList?.[route.params.fieldID]; + const fieldId = `expensify_${route.params.fieldID}`; + const reportField = report?.fieldList?.[fieldId] ?? policy?.fieldList?.[fieldId]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { From aa107372b3ea469dba05181e780f43bf841e0672 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 13 Mar 2024 00:56:08 +0500 Subject: [PATCH 066/189] fix: lint errors --- src/libs/ReportUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7713d7494eae..19d2577df223 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2083,8 +2083,8 @@ function getReportFieldsByPolicyID(policyID: string): Record; + + return fieldList; } /** @@ -2106,10 +2106,10 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); const fields = mergedFieldIds.map((id) => { - const field = report?.fieldList?.[`expensify_${id as string}`]; + const field = report?.fieldList?.[`expensify_${id}`]; if (field) { - return field as PolicyReportField; + return field; } const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); From dff681dfb13a67bc6e8618936ea2288cc8340d1c Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:29:56 +0100 Subject: [PATCH 067/189] add edit tax modal --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + src/libs/PolicyUtils.ts | 8 +- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 97 +++++++++++++++++++ .../workspace/taxes/WorkspaceTaxesPage.tsx | 2 +- 8 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fd30bb0a6ac9..5a9c0cc7ad2a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -601,6 +601,10 @@ const ROUTES = { route: 'workspace/:policyID/taxes/new', getRoute: (policyID: string) => `workspace/${policyID}/taxes/new` as const, }, + WORKSPACE_TAXES_EDIT: { + route: 'workspace/:policyID/tax/:taxID', + getRoute: (policyID: string, taxID: string) => `workspace/${policyID}/tax/${encodeURI(taxID)}` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'workspace/:policyID/distance-rates', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5e3126dfe7f5..11c2d38f4361 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -221,6 +221,7 @@ const SCREENS = { TAGS_EDIT: 'Tags_Edit', TAXES: 'Workspace_Taxes', TAXES_NEW: 'Workspace_Taxes_New', + TAXES_EDIT: 'Workspace_Taxes_Edit', TAG_CREATE: 'Tag_Create', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 2a6a1a0dbb03..164dccbc10ad 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -277,6 +277,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAXES_NEW]: () => require('../../../pages/workspace/taxes/WorkspaceNewTaxPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAXES_EDIT]: () => require('../../../pages/workspace/taxes/WorkspaceEditTaxPage').default as React.ComponentType, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b759ff9e977e..d9fd1fc98c9c 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -318,6 +318,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.TAXES_NEW]: { path: ROUTES.WORKSPACE_TAXES_NEW.route, }, + [SCREENS.WORKSPACE.TAXES_EDIT]: { + path: ROUTES.WORKSPACE_TAXES_EDIT.route, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c8ea81b2b5a7..dafe451262d2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -212,6 +212,10 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.TAXES_NEW]: { policyID: string; }; + [SCREENS.WORKSPACE.TAXES_EDIT]: { + policyID: string; + taxID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d42ad0d56d77..fe6fec83730b 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Navigation from './Navigation/Navigation'; @@ -272,6 +272,11 @@ function goBackFromInvalidPolicy() { Navigation.navigateWithSwitchPolicyID({route: ROUTES.ALL_SETTINGS}); } +/** Get a tax with given ID from policy */ +function getTaxByID(policy: OnyxEntry, taxID: string): TaxRate | undefined { + return policy?.taxRates?.taxes?.[taxID ?? '']; +} + export { getActivePolicies, hasAccountingConnections, @@ -303,6 +308,7 @@ export { getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, hasTaxRateError, + getTaxByID, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx new file mode 100644 index 000000000000..94524ae16cd7 --- /dev/null +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -0,0 +1,97 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import type SCREENS from '@src/SCREENS'; + +type WorkspaceEditTaxPageBaseProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; + +function WorkspaceEditTaxPage({ + route: { + params: {taxID}, + }, + policy, +}: WorkspaceEditTaxPageBaseProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); + const {windowWidth} = useWindowDimensions(); + + const toggle = () => {}; + + const threeDotsMenuItems = useMemo(() => { + const menuItems = [ + { + icon: Expensicons.Trashcan, + text: translate('common.delete'), + onSelected: () => {}, + }, + ]; + return menuItems; + }, [translate]); + + return ( + + + + + {taxID ? ( + // TODO: Extract it to a separate component or use a common one + + + Enable rate + + + + + + ) : null} + {}} + /> + {}} + /> + + + + ); +} + +WorkspaceEditTaxPage.displayName = 'WorkspaceEditTaxPage'; + +export default withPolicyAndFullscreenLoading(WorkspaceEditTaxPage); diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index b0436f20a522..cede2bd31e7d 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -182,7 +182,7 @@ function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) { canSelectMultiple sections={[{data: taxesList, indexOffset: 0, isDisabled: false}]} onCheckboxPress={toggleTax} - onSelectRow={() => {}} + onSelectRow={(tax: ListItem) => tax.keyForList && Navigation.navigate(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policy?.id ?? '', tax.keyForList))} onSelectAll={toggleAllTaxes} showScrollIndicator ListItem={TableListItem} From ff731cbbad482073e39f4638d4ca020a353ae75c Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:45:08 +0100 Subject: [PATCH 068/189] add enabling/disabling taxes --- .../parameters/SetPolicyTaxesEnabledParams.ts | 10 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/actions/TaxRate.ts | 75 ++++++++++++++++++- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 10 ++- 6 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts diff --git a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts new file mode 100644 index 000000000000..0bc8550cd01b --- /dev/null +++ b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts @@ -0,0 +1,10 @@ +type SetPolicyTaxesEnabledParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{taxCode: string, enabled: bool}> + */ + taxFields: string; +}; + +export default SetPolicyTaxesEnabledParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 643657e86614..bfe08dbab50f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -176,3 +176,4 @@ export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflo export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; +export type {default as SetPolicyTaxesEnabledParams} from './SetPolicyTaxesEnabledParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 12c7f3c3bd5a..271aec0ec9be 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -173,6 +173,7 @@ const WRITE_COMMANDS = { ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest', DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', CREATE_POLICY_TAX: 'CreatePolicyTax', + SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled', } as const; type WriteCommand = ValueOf; @@ -344,6 +345,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; + [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 2693f443d659..a7d3fad55788 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -13,7 +13,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.TAXES]: [SCREENS.WORKSPACE.TAXES_NEW], + [SCREENS.WORKSPACE.TAXES]: [SCREENS.WORKSPACE.TAXES_NEW, SCREENS.WORKSPACE.TAXES_EDIT], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 770417e56fe2..3c1f777f315e 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,14 +1,22 @@ +import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {CreatePolicyTaxParams} from '@libs/API/parameters'; +import type {CreatePolicyTaxParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import * as ErrorUtils from '@src/libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {TaxRate} from '@src/types/onyx'; +import type {Policy, TaxRate} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; +let allPolicies: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (value) => (allPolicies = value), +}); + /** * Get tax value with percentage */ @@ -111,4 +119,65 @@ function clearTaxRateError(policyID: string, taxID: string, pendingAction?: Pend }); } -export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage}; +type TaxRateEnabledMap = Record>; + +function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isEnabled: boolean) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const originalTaxes = {...policy?.taxRates?.taxes}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesIDsToUpdate.reduce((acc, taxID) => { + acc[taxID] = {isDisabled: !isEnabled, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}; + return acc; + }, {} as TaxRateEnabledMap), + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesIDsToUpdate.reduce((acc, taxID) => { + acc[taxID] = {isDisabled: !isEnabled, pendingAction: null}; + return acc; + }, {} as TaxRateEnabledMap), + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesIDsToUpdate.reduce((acc, taxID) => { + acc[taxID] = {isDisabled: !!originalTaxes[taxID].isDisabled, pendingAction: null}; + return acc; + }, {} as TaxRateEnabledMap), + }, + }, + }, + ], + }; + + const parameters = { + policyID, + taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: taxID, enabled: isEnabled}))), + } satisfies SetPolicyTaxesEnabledParams; + + console.log({parameters}); + + API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); +} + +export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled}; diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 94524ae16cd7..22706f22faf1 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -10,6 +10,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -29,7 +30,14 @@ function WorkspaceEditTaxPage({ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const {windowWidth} = useWindowDimensions(); - const toggle = () => {}; + const toggle = () => { + // TODO: Backend call doesn't exist yet + return; + if (!policy?.id || !currentTaxRate) { + return; + } + setPolicyTaxesEnabled(policy.id, [taxID], !currentTaxRate?.isDisabled); + }; const threeDotsMenuItems = useMemo(() => { const menuItems = [ From ea97afb6673f59c3112b9a85658538f6fb051c47 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:39:36 +0100 Subject: [PATCH 069/189] add deleting tax rates --- src/languages/en.ts | 2 + .../API/parameters/DeletePolicyTaxesParams.ts | 11 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/TaxRate.ts | 83 +++++++++++++++++-- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 28 ++++++- 6 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/libs/API/parameters/DeletePolicyTaxesParams.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 4ca7b1e059ab..1c5a6931e10b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1856,6 +1856,8 @@ export default { valuePercentageRange: 'Please enter a valid percentage between 0 and 100', genericFailureMessage: 'An error occurred while updating the tax rate, please try again.', }, + deleteTax: 'Delete tax', + deleteTaxConfirmation: 'Are you sure you want to delete this tax?', }, emptyWorkspace: { title: 'Create a workspace', diff --git a/src/libs/API/parameters/DeletePolicyTaxesParams.ts b/src/libs/API/parameters/DeletePolicyTaxesParams.ts new file mode 100644 index 000000000000..fe03d388a129 --- /dev/null +++ b/src/libs/API/parameters/DeletePolicyTaxesParams.ts @@ -0,0 +1,11 @@ +type DeletePolicyTaxesParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + * Each element is a tax name + */ + taxCodes: string; +}; + +export default DeletePolicyTaxesParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index bfe08dbab50f..6567a3e22ad0 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -177,3 +177,4 @@ export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDis export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; export type {default as SetPolicyTaxesEnabledParams} from './SetPolicyTaxesEnabledParams'; +export type {default as DeletePolicyTaxesParams} from './DeletePolicyTaxesParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 271aec0ec9be..98e5d820363a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -174,6 +174,7 @@ const WRITE_COMMANDS = { DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', CREATE_POLICY_TAX: 'CreatePolicyTax', SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled', + DELETE_POLICY_TAXES: 'DeletePolicyTaxes', } as const; type WriteCommand = ValueOf; @@ -346,6 +347,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; + [WRITE_COMMANDS.DELETE_POLICY_TAXES]: Parameters.DeletePolicyTaxesParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 3c1f777f315e..1bc1f3460af1 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,13 +1,13 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {CreatePolicyTaxParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; +import type {CreatePolicyTaxParams, DeletePolicyTaxesParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import * as ErrorUtils from '@src/libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, TaxRate} from '@src/types/onyx'; -import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; let allPolicies: OnyxCollection; @@ -99,7 +99,7 @@ function createWorkspaceTax(policyID: string, taxRate: TaxRate) { API.write(WRITE_COMMANDS.CREATE_POLICY_TAX, parameters, onyxData); } -function clearTaxRateError(policyID: string, taxID: string, pendingAction?: PendingAction) { +function clearTaxRateError(policyID: string, taxID: string, pendingAction?: OnyxCommon.PendingAction) { if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { taxRates: { @@ -175,9 +175,80 @@ function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isE taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: taxID, enabled: isEnabled}))), } satisfies SetPolicyTaxesEnabledParams; - console.log({parameters}); - API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); } -export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled}; +type TaxRateDeleteMap = Record< + string, + | (Pick & { + errors: OnyxCommon.Errors | null; + }) + | null +>; + +/** + * API call to delete policy taxes + * @param taxesToDelete A tax IDs array to delete + */ +function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const policyTaxRates = policy?.taxRates?.taxes; + + if (!policyTaxRates) { + throw new Error('Policy or tax rates not found'); + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesToDelete.reduce((acc, taxID) => { + acc[taxID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: null}; + return acc; + }, {} as TaxRateDeleteMap), + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesToDelete.reduce((acc, taxID) => { + acc[taxID] = null; + return acc; + }, {} as TaxRateDeleteMap), + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesToDelete.reduce((acc, taxID) => { + acc[taxID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.genericFailureMessage')}; + return acc; + }, {} as TaxRateDeleteMap), + }, + }, + }, + ], + }; + + const parameters = { + policyID, + taxCodes: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)), + } as DeletePolicyTaxesParams; + + API.write(WRITE_COMMANDS.DELETE_POLICY_TAXES, parameters, onyxData); +} + +export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled, deletePolicyTaxes}; diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 22706f22faf1..26e36e7f9b38 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -1,6 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -10,7 +11,8 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; +import {deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; +import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -29,6 +31,7 @@ function WorkspaceEditTaxPage({ const {translate} = useLocalize(); const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const {windowWidth} = useWindowDimensions(); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const toggle = () => { // TODO: Backend call doesn't exist yet @@ -39,12 +42,21 @@ function WorkspaceEditTaxPage({ setPolicyTaxesEnabled(policy.id, [taxID], !currentTaxRate?.isDisabled); }; + const deleteTax = () => { + if (!policy?.id) { + return; + } + deletePolicyTaxes(policy?.id, [taxID]); + setIsDeleteModalVisible(false); + Navigation.goBack(); + }; + const threeDotsMenuItems = useMemo(() => { const menuItems = [ { icon: Expensicons.Trashcan, text: translate('common.delete'), - onSelected: () => {}, + onSelected: () => setIsDeleteModalVisible(true), }, ]; return menuItems; @@ -96,6 +108,16 @@ function WorkspaceEditTaxPage({ /> + setIsDeleteModalVisible(false)} + prompt={translate('workspace.taxes.deleteTaxConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> ); } From c47d0e0605b519a09fe0bc71aabca7854ec7d0fc Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 13 Mar 2024 21:09:03 +0530 Subject: [PATCH 070/189] Fix types --- src/CONST.ts | 1 + src/libs/actions/IOU.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fb02dae94c48..4872f51889e4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1302,6 +1302,7 @@ const CONST = { CANCEL: 'cancel', DELETE: 'delete', APPROVE: 'approve', + TRACK: 'track', }, AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 227ed2f9e1b2..23b695b75ee0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1269,7 +1269,7 @@ function getTrackExpenseInformation( // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread const currentTime = DateUtils.getDBTime(); const iouAction = ReportUtils.buildOptimisticIOUReportAction( - CONST.IOU.REPORT_ACTION_TYPE.CREATE, + CONST.IOU.REPORT_ACTION_TYPE.TRACK, amount, currency, comment, @@ -1283,7 +1283,7 @@ function getTrackExpenseInformation( false, currentTime, ); - const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport.reportID); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport); const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); // STEP 5: Build Onyx Data From 7431057cdc96d6509f91d47bb8cc99670f63a79c Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:06:32 +0100 Subject: [PATCH 071/189] fix https://github.com/Expensify/App/pull/37521/#issuecomment-1987648277 --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 99c10550b552..308124e14e73 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -452,7 +452,7 @@ function BaseSelectionList( disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length} /> {!customListHeader ? ( - + {translate('workspace.people.selectAll')} ) : null} From 669099756dc09345a4415495f168604f7df9d2bb Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:53:45 +0100 Subject: [PATCH 072/189] fix https://github.com/Expensify/App/pull/37521/#issuecomment-1987640167 --- .../SelectionList/BaseSelectionList.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 76afb85e588e..fafbcf9b4f80 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -512,28 +512,28 @@ function BaseSelectionList( <> {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - e.preventDefault() : undefined} - > + - {!customListHeader ? ( - + {!customListHeader && ( + e.preventDefault() : undefined} + > {translate('workspace.people.selectAll')} - - ) : null} - + + )} + {customListHeader} )} From f512cf09249c49f58410c90342df6ffa861cb9a9 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:25:43 +0100 Subject: [PATCH 073/189] update enable tax api call --- src/libs/actions/TaxRate.ts | 2 +- src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 1bc1f3460af1..665ceb1da22c 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -172,7 +172,7 @@ function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isE const parameters = { policyID, - taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: taxID, enabled: isEnabled}))), + taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: originalTaxes[taxID].name, enabled: isEnabled}))), } satisfies SetPolicyTaxesEnabledParams; API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 26e36e7f9b38..98d19ecc2b6b 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -34,12 +34,10 @@ function WorkspaceEditTaxPage({ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const toggle = () => { - // TODO: Backend call doesn't exist yet - return; if (!policy?.id || !currentTaxRate) { return; } - setPolicyTaxesEnabled(policy.id, [taxID], !currentTaxRate?.isDisabled); + setPolicyTaxesEnabled(policy.id, [taxID], !!currentTaxRate?.isDisabled); }; const deleteTax = () => { From 2a3f4c33e8262d0e05dc731410e041f24e7d5261 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:39:42 +0100 Subject: [PATCH 074/189] add NamePage and ValuePage --- src/ONYXKEYS.ts | 4 + src/ROUTES.ts | 8 ++ src/SCREENS.ts | 2 + .../AppNavigator/ModalStackNavigators.tsx | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 6 ++ src/libs/Navigation/types.ts | 8 ++ src/pages/workspace/taxes/NamePage.tsx | 85 ++++++++++++++++++ src/pages/workspace/taxes/ValuePage.tsx | 90 +++++++++++++++++++ .../workspace/taxes/WorkspaceEditTaxPage.tsx | 5 +- src/types/form/WorkspaceTaxNameForm.ts | 18 ++++ src/types/form/WorkspaceTaxValueForm.ts | 18 ++++ src/types/form/index.ts | 2 + 13 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/pages/workspace/taxes/NamePage.tsx create mode 100644 src/pages/workspace/taxes/ValuePage.tsx create mode 100644 src/types/form/WorkspaceTaxNameForm.ts create mode 100644 src/types/form/WorkspaceTaxValueForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b4de6c6ef258..900bb9804d29 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -411,6 +411,8 @@ const ONYXKEYS = { POLICY_TAG_NAME_FORM_DRAFT: 'policyTagNameFormDraft', WORKSPACE_NEW_TAX_FORM: 'workspaceNewTaxForm', WORKSPACE_NEW_TAX_FORM_DRAFT: 'workspaceNewTaxFormDraft', + WORKSPACE_TAX_NAME_FORM: 'workspaceTaxNameForm', + WORKSPACE_TAX_VALUE_FORM: 'workspaceTaxValueForm', }, } as const; @@ -459,6 +461,8 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; [ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm; [ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM]: FormTypes.WorkspaceNewTaxForm; + [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; + [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5a9c0cc7ad2a..2487102bc504 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -605,6 +605,14 @@ const ROUTES = { route: 'workspace/:policyID/tax/:taxID', getRoute: (policyID: string, taxID: string) => `workspace/${policyID}/tax/${encodeURI(taxID)}` as const, }, + WORKSPACE_TAXES_NAME: { + route: 'workspace/:policyID/tax/:taxID/name', + getRoute: (policyID: string, taxID: string) => `workspace/${policyID}/tax/${encodeURI(taxID)}/name` as const, + }, + WORKSPACE_TAXES_VALUE: { + route: 'workspace/:policyID/tax/:taxID/value', + getRoute: (policyID: string, taxID: string) => `workspace/${policyID}/tax/${encodeURI(taxID)}/value` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'workspace/:policyID/distance-rates', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 11c2d38f4361..d8eb643cefde 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -222,6 +222,8 @@ const SCREENS = { TAXES: 'Workspace_Taxes', TAXES_NEW: 'Workspace_Taxes_New', TAXES_EDIT: 'Workspace_Taxes_Edit', + TAXES_NAME: 'Workspace_Taxes_Name', + TAXES_VALUE: 'Workspace_Taxes_Value', TAG_CREATE: 'Tag_Create', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 164dccbc10ad..41fe1b298576 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -278,6 +278,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAXES_NEW]: () => require('../../../pages/workspace/taxes/WorkspaceNewTaxPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAXES_EDIT]: () => require('../../../pages/workspace/taxes/WorkspaceEditTaxPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAXES_NAME]: () => require('../../../pages/workspace/taxes/NamePage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAXES_VALUE]: () => require('../../../pages/workspace/taxes/ValuePage').default as React.ComponentType, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index a7d3fad55788..9eb35121413c 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -13,7 +13,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.TAXES]: [SCREENS.WORKSPACE.TAXES_NEW, SCREENS.WORKSPACE.TAXES_EDIT], + [SCREENS.WORKSPACE.TAXES]: [SCREENS.WORKSPACE.TAXES_NEW, SCREENS.WORKSPACE.TAXES_EDIT, SCREENS.WORKSPACE.TAXES_NAME, SCREENS.WORKSPACE.TAXES_VALUE], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index d9fd1fc98c9c..cfef83bf9c1c 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -321,6 +321,12 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.TAXES_EDIT]: { path: ROUTES.WORKSPACE_TAXES_EDIT.route, }, + [SCREENS.WORKSPACE.TAXES_NAME]: { + path: ROUTES.WORKSPACE_TAXES_NAME.route, + }, + [SCREENS.WORKSPACE.TAXES_VALUE]: { + path: ROUTES.WORKSPACE_TAXES_VALUE.route, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index dafe451262d2..f93b52657acf 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -216,6 +216,14 @@ type SettingsNavigatorParamList = { policyID: string; taxID: string; }; + [SCREENS.WORKSPACE.TAXES_NAME]: { + policyID: string; + taxID: string; + }; + [SCREENS.WORKSPACE.TAXES_VALUE]: { + policyID: string; + taxID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx new file mode 100644 index 000000000000..d7626f5dca5c --- /dev/null +++ b/src/pages/workspace/taxes/NamePage.tsx @@ -0,0 +1,85 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; +import React, {useState} from 'react'; +import {View} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {renamePolicyTax} from '@libs/actions/TaxRate'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceTaxNameForm'; +import type * as OnyxTypes from '@src/types/onyx'; + +type NamePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; + +const parser = new ExpensiMark(); + +function NamePage({ + route: { + params: {policyID, taxID}, + }, + policy, +}: NamePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); + + const [name, setName] = useState(() => parser.htmlToMarkdown(currentTaxRate?.name ?? '')); + + const submit = () => { + renamePolicyTax(policyID, taxID, name); + Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)); + }; + + return ( + + + + + + + + + + ); +} + +NamePage.displayName = 'NamePage'; + +export default withPolicyAndFullscreenLoading(NamePage); diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx new file mode 100644 index 000000000000..5a390f27dacf --- /dev/null +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -0,0 +1,90 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useState} from 'react'; +import AmountForm from '@components/AmountForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {renamePolicyTax, updatePolicyTaxValue} from '@libs/actions/TaxRate'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceTaxValueForm'; +import type * as OnyxTypes from '@src/types/onyx'; + +type ValuePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; + +function ValuePage({ + route: { + params: {policyID, taxID}, + }, + policy, +}: ValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); + const [value, setValue] = useState(currentTaxRate?.value?.replace('%', '')); + + // TODO: Extract it to a separate file, and use it also when creating a new tax + const validate = useCallback((values: FormOnyxValues) => { + const errors = {}; + + if (Number(values.value) < 0 || Number(values.value) >= 100) { + ErrorUtils.addErrorMessage(errors, 'value', 'Percentage must be between 0 and 100'); + } + + return errors; + }, []); + + const submit = useCallback( + (values: FormOnyxValues) => { + updatePolicyTaxValue(policyID, taxID, `${values.value}%`); + Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)); + }, + [policyID, taxID], + ); + + return ( + + + + + %} + /> + + + ); +} + +ValuePage.displayName = 'ValuePage'; + +export default withPolicyAndFullscreenLoading(ValuePage); diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 98d19ecc2b6b..2617b710f55c 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -17,6 +17,7 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; type WorkspaceEditTaxPageBaseProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; @@ -94,7 +95,7 @@ function WorkspaceEditTaxPage({ description={translate('common.name')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => {}} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAXES_NAME.getRoute(`${policy?.id}`, taxID))} /> {}} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAXES_VALUE.getRoute(`${policy?.id}`, taxID))} /> diff --git a/src/types/form/WorkspaceTaxNameForm.ts b/src/types/form/WorkspaceTaxNameForm.ts new file mode 100644 index 000000000000..dfe01ab55fae --- /dev/null +++ b/src/types/form/WorkspaceTaxNameForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + NAME: 'name', +} as const; + +type InputID = ValueOf; + +type WorkspaceTaxNameForm = Form< + InputID, + { + [INPUT_IDS.NAME]: string; + } +>; + +export type {WorkspaceTaxNameForm}; +export default INPUT_IDS; diff --git a/src/types/form/WorkspaceTaxValueForm.ts b/src/types/form/WorkspaceTaxValueForm.ts new file mode 100644 index 000000000000..e53c6cd46cc2 --- /dev/null +++ b/src/types/form/WorkspaceTaxValueForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + VALUE: 'value', +} as const; + +type InputID = ValueOf; + +type WorkspaceTaxValueForm = Form< + InputID, + { + [INPUT_IDS.VALUE]: string; + } +>; + +export type {WorkspaceTaxValueForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 8beada7ad6a8..7df684ccbd3e 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -40,5 +40,7 @@ export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm'; export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm'; export type {PolicyTagNameForm} from './PolicyTagNameForm'; export type {WorkspaceNewTaxForm} from './WorkspaceNewTaxForm'; +export type {WorkspaceTaxNameForm} from './WorkspaceTaxNameForm'; +export type {WorkspaceTaxValueForm} from './WorkspaceTaxValueForm'; export type {WorkspaceTagCreateForm} from './WorkspaceTagCreateForm'; export type {default as Form} from './Form'; From a25926f7f41b07d74eddf4173fd90c145cd39109 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:23:02 +0100 Subject: [PATCH 075/189] renaming tax names --- .../parameters/UpdatePolicyTaxValueParams.ts | 7 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 3 + src/libs/actions/TaxRate.ts | 135 +++++++++++++++++- src/pages/workspace/taxes/ValuePage.tsx | 2 +- 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 src/libs/API/parameters/UpdatePolicyTaxValueParams.ts diff --git a/src/libs/API/parameters/UpdatePolicyTaxValueParams.ts b/src/libs/API/parameters/UpdatePolicyTaxValueParams.ts new file mode 100644 index 000000000000..1124755ea9ef --- /dev/null +++ b/src/libs/API/parameters/UpdatePolicyTaxValueParams.ts @@ -0,0 +1,7 @@ +type UpdatePolicyTaxValueParams = { + policyID: string; + taxCode: string; + taxAmount: number; +}; + +export default UpdatePolicyTaxValueParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f67ea4690e7d..6ab1eed97d16 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -180,3 +180,4 @@ export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMore export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; export type {default as SetPolicyTaxesEnabledParams} from './SetPolicyTaxesEnabledParams'; export type {default as DeletePolicyTaxesParams} from './DeletePolicyTaxesParams'; +export type {default as UpdatePolicyTaxValueParams} from './UpdatePolicyTaxValueParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 5aa0f6a18599..ab2d17ebd4b9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -176,6 +176,8 @@ const WRITE_COMMANDS = { CREATE_POLICY_TAX: 'CreatePolicyTax', SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled', DELETE_POLICY_TAXES: 'DeletePolicyTaxes', + UPDATE_POLICY_TAX_VALUE: 'UpdatePolicyTaxValue', + RENAME_POLICY_TAX: 'RenamePolicyTax', } as const; type WriteCommand = ValueOf; @@ -350,6 +352,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; [WRITE_COMMANDS.DELETE_POLICY_TAXES]: Parameters.DeletePolicyTaxesParams; + [WRITE_COMMANDS.UPDATE_POLICY_TAX_VALUE]: Parameters.UpdatePolicyTaxValueParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 665ceb1da22c..41c7800be5c6 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,7 +1,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {CreatePolicyTaxParams, DeletePolicyTaxesParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; +import type {CreatePolicyTaxParams, DeletePolicyTaxesParams, SetPolicyTaxesEnabledParams, UpdatePolicyTaxValueParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import * as ErrorUtils from '@src/libs/ErrorUtils'; @@ -251,4 +251,135 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { API.write(WRITE_COMMANDS.DELETE_POLICY_TAXES, parameters, onyxData); } -export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled, deletePolicyTaxes}; +/** + * Rename policy tax + */ +function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const originalTaxRate = {...policy?.taxRates?.taxes[taxID]}; + const stringTaxValue = `${taxValue}%`; + + console.log({policy, originalTaxRate, stringTaxValue, taxValue, taxID}); + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: { + [taxID]: { + value: stringTaxValue, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: null, + }, + }, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: { + [taxID]: {value: stringTaxValue, pendingAction: null, errors: null}, + }, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: { + [taxID]: {value: originalTaxRate.value, pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.genericFailureMessage')}, + }, + }, + }, + }, + ], + }; + + if (!originalTaxRate.name) { + throw new Error('Tax rate name not found'); + } + + const parameters = { + policyID, + taxCode: originalTaxRate.name, + taxAmount: Number(taxValue), + } as UpdatePolicyTaxValueParams; + + API.write(WRITE_COMMANDS.UPDATE_POLICY_TAX_VALUE, parameters, onyxData); +} + +function renamePolicyTax(policyID: string, taxID: string, newName: string) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const originalTaxRate = {...policy?.taxRates?.taxes[taxID]}; + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: { + [taxID]: { + name: newName, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + errors: null, + }, + }, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: { + [taxID]: {name: newName, pendingAction: null, errors: null}, + }, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: { + [taxID]: {name: originalTaxRate.name, pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.genericFailureMessage')}, + }, + }, + }, + }, + ], + }; + + if (!originalTaxRate.name) { + throw new Error('Tax rate name not found'); + } + + const parameters = { + policyID, + taxCode: taxID, + newName, + }; + + API.write(WRITE_COMMANDS.RENAME_POLICY_TAX, parameters, onyxData); +} + +export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled, deletePolicyTaxes, updatePolicyTaxValue, renamePolicyTax}; diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx index 5a390f27dacf..6c733968aa5e 100644 --- a/src/pages/workspace/taxes/ValuePage.tsx +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -48,7 +48,7 @@ function ValuePage({ const submit = useCallback( (values: FormOnyxValues) => { - updatePolicyTaxValue(policyID, taxID, `${values.value}%`); + updatePolicyTaxValue(policyID, taxID, Number(values.value)); Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)); }, [policyID, taxID], From ac59d758b8c8bb69978fbfca5326c974acc8350a Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:45:03 +0100 Subject: [PATCH 076/189] update backend queries --- src/libs/actions/TaxRate.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 41c7800be5c6..50a8ed0a1bdc 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -86,14 +86,12 @@ function createWorkspaceTax(policyID: string, taxRate: TaxRate) { const parameters = { policyID, - taxFields: JSON.stringify([ - { - name: taxRate.name, - value: taxRate.value, - enabled: true, - taxCode: taxRate.code, - }, - ]), + taxFields: JSON.stringify({ + name: taxRate.name, + value: taxRate.value, + enabled: true, + taxCode: taxRate.code, + }), } satisfies CreatePolicyTaxParams; API.write(WRITE_COMMANDS.CREATE_POLICY_TAX, parameters, onyxData); @@ -313,7 +311,7 @@ function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) const parameters = { policyID, - taxCode: originalTaxRate.name, + taxCode: taxID, taxAmount: Number(taxValue), } as UpdatePolicyTaxValueParams; From 139d14bd20e1c53372e6ebac77af079637a13d9c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 14 Mar 2024 11:59:40 +0100 Subject: [PATCH 077/189] remove lint disable --- src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx index d5799e617226..05424c9975c5 100644 --- a/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx +++ b/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx @@ -1,4 +1,3 @@ -/* eslint-disable rulesdir/no-negated-variables */ import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; From 7d9e274c061dbf660de98238f0379ac0a290db0a Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 14 Mar 2024 13:11:07 +0200 Subject: [PATCH 078/189] solved conflict --- src/libs/desktopLoginRedirect/index.desktop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/desktopLoginRedirect/index.desktop.ts b/src/libs/desktopLoginRedirect/index.desktop.ts index e751fa1ffd78..ccc442346dc1 100644 --- a/src/libs/desktopLoginRedirect/index.desktop.ts +++ b/src/libs/desktopLoginRedirect/index.desktop.ts @@ -9,7 +9,7 @@ function desktopLoginRedirect(autoAuthState: AutoAuthState, isSignedIn: boolean) const shouldPopToTop = (autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN) && !isSignedIn; if (shouldPopToTop) { - Navigation.isNavigationReady().then(() => Navigation.popToTop()); + Navigation.isNavigationReady().then(() => Navigation.resetToHome()); } } From 476445981b2b41762a3230e14c3ee43415f9f4b3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 14 Mar 2024 12:15:00 +0100 Subject: [PATCH 079/189] fix nav type --- .../workflows/WorkspaceAutoReportingFrequencyPage.tsx | 4 ++-- .../workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx | 5 +++-- .../workspace/workflows/WorkspaceWorkflowsApproverPage.tsx | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index 5a7e7a2fc3a9..d9997a1aefca 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -12,7 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; -import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicy from '@pages/workspace/withPolicy'; @@ -26,7 +26,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AutoReportingFrequencyKey = Exclude, 'instant'>; type Locale = ValueOf; -type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & StackScreenProps; +type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & StackScreenProps; type WorkspaceAutoReportingFrequencyPageItem = { text: string; diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 3aad5fb9f0a4..834765cb331b 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -8,7 +8,7 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicy from '@pages/workspace/withPolicy'; @@ -20,7 +20,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; const DAYS_OF_MONTH = 28; -type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps & StackScreenProps; +type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps & + StackScreenProps; type AutoReportingOffsetKeys = ValueOf; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index 34c0f8989888..7475d0f195aa 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -18,7 +18,7 @@ import compose from '@libs/compose'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -40,7 +40,7 @@ type WorkspaceWorkflowsApproverPageOnyxProps = { type WorkspaceWorkflowsApproverPageProps = WorkspaceWorkflowsApproverPageOnyxProps & WithPolicyAndFullscreenLoadingProps & - StackScreenProps; + StackScreenProps; type MemberOption = Omit & {accountID: number}; type MembersSection = SectionListData>; From 1b2779543d9aaa6e06bb91235b2774d8f00d8c86 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 14 Mar 2024 12:37:44 +0100 Subject: [PATCH 080/189] fix prettier --- tests/unit/MigrationTest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index 147588559e13..c6513671776b 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -6,10 +6,9 @@ import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviou import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; -import { toCollectionDataSet } from '@src/types/utils/CollectionDataSet'; +import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; - jest.mock('@src/libs/getPlatform'); let LogSpy: jest.SpyInstance>; From 76d56f8f74d33f8fa09184a470b8b993804aae10 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:40:08 +0100 Subject: [PATCH 081/189] refactor --- src/pages/workspace/taxes/NamePage.tsx | 22 +++++++++++++--------- src/pages/workspace/taxes/ValuePage.tsx | 14 +++++++++----- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx index d7626f5dca5c..c6c4f04177f1 100644 --- a/src/pages/workspace/taxes/NamePage.tsx +++ b/src/pages/workspace/taxes/NamePage.tsx @@ -1,12 +1,13 @@ import type {StackScreenProps} from '@react-navigation/stack'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {renamePolicyTax} from '@libs/actions/TaxRate'; @@ -20,7 +21,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceTaxNameForm'; -import type * as OnyxTypes from '@src/types/onyx'; type NamePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; @@ -35,12 +35,15 @@ function NamePage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); + const {inputCallbackRef} = useAutoFocusInput(); const [name, setName] = useState(() => parser.htmlToMarkdown(currentTaxRate?.name ?? '')); + const goBack = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)), [policyID, taxID]); + const submit = () => { renamePolicyTax(policyID, taxID, name); - Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)); + goBack(); }; return ( @@ -49,7 +52,10 @@ function NamePage({ shouldEnableMaxHeight testID={NamePage.displayName} > - + diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx index 6c733968aa5e..3aa990db13c3 100644 --- a/src/pages/workspace/taxes/ValuePage.tsx +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -9,7 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {renamePolicyTax, updatePolicyTaxValue} from '@libs/actions/TaxRate'; +import {updatePolicyTaxValue} from '@libs/actions/TaxRate'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -20,7 +20,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceTaxValueForm'; -import type * as OnyxTypes from '@src/types/onyx'; type ValuePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; @@ -35,6 +34,8 @@ function ValuePage({ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const [value, setValue] = useState(currentTaxRate?.value?.replace('%', '')); + const goBack = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)), [policyID, taxID]); + // TODO: Extract it to a separate file, and use it also when creating a new tax const validate = useCallback((values: FormOnyxValues) => { const errors = {}; @@ -49,9 +50,9 @@ function ValuePage({ const submit = useCallback( (values: FormOnyxValues) => { updatePolicyTaxValue(policyID, taxID, Number(values.value)); - Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)); + goBack(); }, - [policyID, taxID], + [goBack, policyID, taxID], ); return ( @@ -60,7 +61,10 @@ function ValuePage({ shouldEnableMaxHeight testID={ValuePage.displayName} > - + Date: Thu, 14 Mar 2024 12:45:10 +0100 Subject: [PATCH 082/189] update to new backend --- src/libs/API/parameters/DeletePolicyTaxesParams.ts | 2 +- src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts | 2 +- src/libs/actions/TaxRate.ts | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/API/parameters/DeletePolicyTaxesParams.ts b/src/libs/API/parameters/DeletePolicyTaxesParams.ts index fe03d388a129..9e0963cdcb28 100644 --- a/src/libs/API/parameters/DeletePolicyTaxesParams.ts +++ b/src/libs/API/parameters/DeletePolicyTaxesParams.ts @@ -5,7 +5,7 @@ type DeletePolicyTaxesParams = { * Array * Each element is a tax name */ - taxCodes: string; + taxNames: string; }; export default DeletePolicyTaxesParams; diff --git a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts index 0bc8550cd01b..4ed0a05cfdec 100644 --- a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts +++ b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts @@ -4,7 +4,7 @@ type SetPolicyTaxesEnabledParams = { * Stringified JSON object with type of following structure: * Array<{taxCode: string, enabled: bool}> */ - taxFields: string; + taxFieldsArray: string; }; export default SetPolicyTaxesEnabledParams; diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 50a8ed0a1bdc..ced92e12e4b8 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -170,7 +170,7 @@ function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isE const parameters = { policyID, - taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: originalTaxes[taxID].name, enabled: isEnabled}))), + taxFieldsArray: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: originalTaxes[taxID].name, enabled: isEnabled}))), } satisfies SetPolicyTaxesEnabledParams; API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); @@ -243,7 +243,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const parameters = { policyID, - taxCodes: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)), + taxNames: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)), } as DeletePolicyTaxesParams; API.write(WRITE_COMMANDS.DELETE_POLICY_TAXES, parameters, onyxData); @@ -257,8 +257,6 @@ function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) const originalTaxRate = {...policy?.taxRates?.taxes[taxID]}; const stringTaxValue = `${taxValue}%`; - console.log({policy, originalTaxRate, stringTaxValue, taxValue, taxID}); - const onyxData: OnyxData = { optimisticData: [ { From 3b590c672aa27e32ef4f76c9c272e9126a9834e5 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:07:26 +0100 Subject: [PATCH 083/189] add bulk actions --- src/CONST.ts | 5 ++ .../ButtonWithDropdownMenu/types.ts | 4 +- src/languages/en.ts | 6 +- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 2 +- .../workspace/taxes/WorkspaceTaxesPage.tsx | 73 +++++++++++++++---- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index cf0d6ac57a08..bb04b27dc1a2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1422,6 +1422,11 @@ const CONST = { DISABLE: 'disable', ENABLE: 'enable', }, + TAX_RATES_BULK_ACTION_TYPES: { + DELETE: 'delete', + DISABLE: 'disable', + ENABLE: 'enable', + }, }, CUSTOM_UNITS: { diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 798369292958..83100788761f 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -12,6 +12,8 @@ type WorkspaceMemberBulkActionType = DeepValueOf; +type WorkspaceTaxRatesBulkActionType = DeepValueOf; + type DropdownOption = { value: TValueType; text: string; @@ -73,4 +75,4 @@ type ButtonWithDropdownMenuProps = { wrapperStyle?: StyleProp; }; -export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps}; +export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType}; diff --git a/src/languages/en.ts b/src/languages/en.ts index d60861a838be..876399e9a864 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1860,8 +1860,12 @@ export default { valuePercentageRange: 'Please enter a valid percentage between 0 and 100', genericFailureMessage: 'An error occurred while updating the tax rate, please try again.', }, - deleteTax: 'Delete tax', deleteTaxConfirmation: 'Are you sure you want to delete this tax?', + actions: { + delete: 'Delete rate', + disable: 'Disable rate', + enable: 'Enable rate', + }, }, emptyWorkspace: { title: 'Create a workspace', diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 2617b710f55c..e785790d64e4 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -108,7 +108,7 @@ function WorkspaceEditTaxPage({ setIsDeleteModalVisible(false)} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index cede2bd31e7d..04529014b2ac 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -2,6 +2,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption, WorkspaceTaxRatesBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -129,23 +131,64 @@ function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) { ); + const dropdownMenuOptions = useMemo(() => { + const options: Array> = [ + { + icon: Expensicons.Trashcan, + text: translate('workspace.taxes.actions.delete'), + value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DELETE, + onSelected: () => {}, + }, + ]; + + // `Disable rates` when at least one enabled rate is selected. + if (selectedTaxesIDs.some((taxID) => !policy?.taxRates?.taxes[taxID]?.isDisabled)) { + options.push({ + icon: Expensicons.Document, + text: translate('workspace.taxes.actions.disable'), + value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DISABLE, + }); + } + + // `Enable rates` when at least one disabled rate is selected. + if (selectedTaxesIDs.some((taxID) => policy?.taxRates?.taxes[taxID]?.isDisabled)) { + options.push({ + icon: Expensicons.Document, + text: translate('workspace.taxes.actions.enable'), + value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.ENABLE, + }); + } + return options; + }, [policy?.taxRates?.taxes, selectedTaxesIDs, translate]); + const headerButtons = ( -