+
+1. Click the **Expenses** tab.
+2. Select the expenses you want to export by checking the box to the left of each expense or selecting them all.
+3. Click **Export To** in the right corner and select either:
+ - **Default CSV**: Use Expensify’s default template
+ - **Create new CSV export layout**: Create your own custom CSV template
+
+
diff --git a/docs/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account.md b/docs/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account.md
new file mode 100644
index 000000000000..9037e58661d1
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account.md
@@ -0,0 +1,30 @@
+---
+title: Assign billing owner and payment account
+description: Determine who will cover the cost of the workspace and link a payment method
+---
+
+
+The person who creates a workspace will automatically be responsible for the billing for that workspace. However, the existing billing owner can transfer the workspace’s billing ownership to any Admin on the workspace.
+
+{% include info.html %}
+There can only be one billing owner at a time. Assigning a new billing owner will automatically un-assign the existing billing owner. However, billing owners are also workspace admins by default, and the previous billing owner will remain a workspace admin unless manually updated.
+{% include end-info.html %}
+
+# Assign a new billing owner
+
+To assign a new billing owner, **the person who will take over responsibility for the workspace billing must complete the following process**:
+
+1. Hover over Settings, then click **Workspaces**.
+2. Click the desired workspace name.
+3. Under Workspace Overview, click **Take Over Billing**.
+
+# Add or update payment account
+
+Once you take over billing for a workspace, you must add a payment method to your account.
+
+1. Hover over Settings, then click **Account**.
+2. Click the **Payments** tab.
+3. Scroll down to the Payment Details sections and click **Add Payment Card**.
+4. Enter your credit or debit card information and click **Accept terms, add payment card, and pay $0.00** (the box will only show a balance if one is due).
+
+
diff --git a/docs/articles/expensify-classic/workspaces/Create-a-group-workspace.md b/docs/articles/expensify-classic/workspaces/Create-a-group-workspace.md
new file mode 100644
index 000000000000..b0b016afbcbb
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Create-a-group-workspace.md
@@ -0,0 +1,24 @@
+---
+title: Create a group workspace
+description: Create a workspace for your team's expense reports
+---
+
+
+A workspace is the set of rules, settings, and spending limits for expense reports in your organization. This includes the unique expense categories and tags, budgets, currency and tax settings, etc. that all workspace members will use. A workspace also defines the approval workflow for your employees, as well as the accounting connection if using an accounting software integration.
+
+Here are a couple examples of when you’d want to create different workspaces:
+
+- You have employees with expense reports in different currencies. For example, you may have a workspace for employees who live in the US and submit their reports in USD and a workspace for employees who live in Canada and submit in CAD.
+- You want to limit specific groups of people to their own set of expense coding options (categories/tags) then they can separate their employees by Sales, Marketing, Support, etc.
+
+To create a group workspace,
+
+1. Hover over Settings, then click **Workspaces**.
+2. Click the **Group** tab on the left.
+3. Click **New Workspace**.
+4. Enter the workspace name and select a workspace type.
+ - **Collect**: Ideal for small groups who only need basic features like expense approvals, reimbursement, corporate card management, and integration options.
+ - **Control**: For groups that need a deeper level of control and configurations, like multi-stage approval workflows, corporate card management, integrations, and more. This is the most popular option.
+5. Set up your workspace details including the workspace name, expense rules, categories, and more.
+
+
diff --git a/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md b/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md
new file mode 100644
index 000000000000..c8be9a2728d5
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md
@@ -0,0 +1,20 @@
+---
+title: Set up your individual workspace
+description: Capture your personal expenses
+---
+
+
+All Expensify accounts come with an individual workspace where you can track your personal expenses. If you want to connect your personal expenses to an accounting or travel integration, you can create a group workspace—even if you will be the only person in the group.
+
+To set up your individual workspace,
+
+1. Hover over Settings, then click **Workspaces**.
+2. Click the **Individual** tab on the left.
+3. Select the policy type that best fits your needs.
+4. Set up your workspace details including the workspace name, expense rules, categories, and more.
+
+{% include info.html %}
+You can create multiple group workspaces, but you can only create one individual workspace.
+{% include end-info.html %}
+
+
diff --git a/docs/redirects.csv b/docs/redirects.csv
index df4e2a45dce3..7539a2777d92 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -66,3 +66,4 @@ https://help.expensify.com/articles/expensify-classic/expensify-billing/Individu
https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support
+https://help.expensify.com/articles/expensify-classic/settings/Copilot,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 22de50aee5a3..5e2ba1fcd614 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.55.1
+ 1.4.55.3ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 61c240540779..69472200e46d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature????CFBundleVersion
- 1.4.55.1
+ 1.4.55.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index a30258647997..008ca16909b0 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString1.4.55CFBundleVersion
- 1.4.55.1
+ 1.4.55.3NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 0d0b11fff3f1..4bff5eaf6eb8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.55-1",
+ "version": "1.4.55-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.55-1",
+ "version": "1.4.55-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -51,7 +51,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#615f4a8662cd1abea9fdeee4d04847197c5e36ae",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4e020cfa13ffabde14313c92b341285aeb919f29",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -70,7 +70,7 @@
"react": "18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-collapse": "^5.1.0",
- "react-content-loader": "^6.1.0",
+ "react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
"react-map-gl": "^7.1.3",
@@ -23087,9 +23087,9 @@
}
},
"node_modules/classnames": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz",
- "integrity": "sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA=="
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz",
+ "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA=="
},
"node_modules/clean-css": {
"version": "5.3.2",
@@ -27370,11 +27370,11 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#615f4a8662cd1abea9fdeee4d04847197c5e36ae",
- "integrity": "sha512-k/SmW3EBR+gxFkJP/59LJsmBKjnKR07XS30yk/GkQ0lIfyYkNmFJ0dWm/S/54ezFweezR7MDaQ3zGc45Mb/U5A==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#4e020cfa13ffabde14313c92b341285aeb919f29",
+ "integrity": "sha512-sx3cIYkmiydNaXRe4kJebPyEje8HfssUbsoB6uW8vvMLwFheCZfkmF9fRMBNLo8BQsfWIstT5TApEhwuWPjqZg==",
"license": "MIT",
"dependencies": {
- "classnames": "2.4.0",
+ "classnames": "2.5.0",
"clipboard": "2.0.11",
"html-entities": "^2.4.0",
"jquery": "3.6.0",
@@ -27383,7 +27383,7 @@
"prop-types": "15.8.1",
"react": "16.12.0",
"react-dom": "16.12.0",
- "semver": "^7.5.2",
+ "semver": "^7.6.0",
"simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
"ua-parser-js": "^1.0.37",
"underscore": "1.13.6"
@@ -38828,8 +38828,9 @@
}
},
"node_modules/react-content-loader": {
- "version": "6.2.0",
- "license": "MIT",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-7.0.0.tgz",
+ "integrity": "sha512-xaBwpO7eiJyEc4ndym+g6wcruV9W2y3DKqbw4U48QFBsv0IeAVZO+aCUb8GptlDLWM8n5zi2HcFSGlj5r+53Tg==",
"engines": {
"node": ">=10"
},
diff --git a/package.json b/package.json
index 5001e6f0dd1a..53eb229d7b85 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.55-1",
+ "version": "1.4.55-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -102,7 +102,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#615f4a8662cd1abea9fdeee4d04847197c5e36ae",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4e020cfa13ffabde14313c92b341285aeb919f29",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -121,7 +121,7 @@
"react": "18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-collapse": "^5.1.0",
- "react-content-loader": "^6.1.0",
+ "react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
"react-map-gl": "^7.1.3",
diff --git a/patches/react-native-web+0.19.9+006+fixPointerEventDown.patch b/patches/react-native-web+0.19.9+006+fixPointerEventDown.patch
new file mode 100644
index 000000000000..e6a3822836f4
--- /dev/null
+++ b/patches/react-native-web+0.19.9+006+fixPointerEventDown.patch
@@ -0,0 +1,43 @@
+diff --git a/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js b/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js
+index 0aec2d6..a71aec2 100644
+--- a/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js
++++ b/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js
+@@ -133,7 +133,7 @@ to return true:wantsResponderID| |
+
+ import createResponderEvent from './createResponderEvent';
+ import { isCancelish, isEndish, isMoveish, isScroll, isSelectionChange, isStartish } from './ResponderEventTypes';
+-import { getLowestCommonAncestor, getResponderPaths, hasTargetTouches, hasValidSelection, isPrimaryPointerDown, setResponderId } from './utils';
++import { getLowestCommonAncestor, getResponderPaths, hasTargetTouches, hasValidSelection, isPrimaryOrSecondaryPointerDown, setResponderId } from './utils';
+ import { ResponderTouchHistoryStore } from './ResponderTouchHistoryStore';
+ import canUseDOM from '../canUseDom';
+
+@@ -225,7 +225,7 @@ function eventListener(domEvent) {
+ }
+ return;
+ }
+- var isStartEvent = isStartish(eventType) && isPrimaryPointerDown(domEvent);
++ var isStartEvent = isStartish(eventType) && isPrimaryOrSecondaryPointerDown(domEvent);
+ var isMoveEvent = isMoveish(eventType);
+ var isEndEvent = isEndish(eventType);
+ var isScrollEvent = isScroll(eventType);
+diff --git a/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js b/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js
+index 7382cdd..d88f6c0 100644
+--- a/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js
++++ b/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js
+@@ -148,14 +148,14 @@ export function hasValidSelection(domEvent) {
+ /**
+ * Events are only valid if the primary button was used without specific modifier keys.
+ */
+-export function isPrimaryPointerDown(domEvent) {
++export function isPrimaryOrSecondaryPointerDown(domEvent) {
+ var altKey = domEvent.altKey,
+ button = domEvent.button,
+ buttons = domEvent.buttons,
+ ctrlKey = domEvent.ctrlKey,
+ type = domEvent.type;
+ var isTouch = type === 'touchstart' || type === 'touchmove';
+- var isPrimaryMouseDown = type === 'mousedown' && (button === 0 || buttons === 1);
++ var isPrimaryMouseDown = type === 'mousedown' && (button === 0 || buttons === 1 || buttons === 2);
+ var isPrimaryMouseMove = type === 'mousemove' && buttons === 1;
+ var noModifiers = altKey === false && ctrlKey === false;
+ if (isTouch || isPrimaryMouseDown && noModifiers || isPrimaryMouseMove && noModifiers) {
diff --git a/src/CONST.ts b/src/CONST.ts
index 3c53f083abac..af4864c22a85 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -630,6 +630,7 @@ const CONST = {
EXPORTEDTOQUICKBOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action
FORWARDED: 'FORWARDED', // OldDot Action
HOLD: 'HOLD',
+ HOLDCOMMENT: 'HOLDCOMMENT',
IOU: 'IOU',
INTEGRATIONSMESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action
MANAGERATTACHRECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 5681be838ca8..026025593aef 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -25,12 +25,10 @@ import Navigation from './libs/Navigation/Navigation';
import NavigationRoot from './libs/Navigation/NavigationRoot';
import NetworkConnection from './libs/NetworkConnection';
import PushNotification from './libs/Notification/PushNotification';
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
import './libs/Notification/PushNotification/subscribePushNotification';
import StartupTimer from './libs/StartupTimer';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import UnreadIndicatorUpdater from './libs/UnreadIndicatorUpdater';
+import './libs/UnreadIndicatorUpdater';
import Visibility from './libs/Visibility';
import ONYXKEYS from './ONYXKEYS';
import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu';
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index d74e691fe10e..d3fab1b9fcde 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -353,6 +353,8 @@ const ONYXKEYS = {
WORKSPACE_TAX_CUSTOM_NAME_DRAFT: 'workspaceTaxCustomNameDraft',
POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm',
POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft',
+ POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm',
+ POLICY_DISTANCE_RATE_EDIT_FORM_DRAFT: 'policyDistanceRateEditFormDraft',
CLOSE_ACCOUNT_FORM: 'closeAccount',
CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft',
PROFILE_SETTINGS_FORM: 'profileSettingsForm',
@@ -482,6 +484,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm;
[ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM]: FormTypes.WorkspaceNewTaxForm;
[ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm;
+ [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm;
};
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 1f802c5036e3..c216d5ac288c 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -204,7 +204,7 @@ const ROUTES = {
},
REPORT_ATTACHMENTS: {
route: 'r/:reportID/attachment',
- getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}` as const,
+ getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const,
},
REPORT_PARTICIPANTS: {
route: 'r/:reportID/participants',
@@ -304,13 +304,10 @@ const ROUTES = {
route: ':iouType/new/receipt/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
},
- MONEY_REQUEST_DISTANCE: {
- route: ':iouType/new/address/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const,
- },
MONEY_REQUEST_CREATE: {
- route: 'create/:iouType/start/:transactionID/:reportID',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const,
+ route: ':action/:iouType/start/:transactionID/:reportID',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
+ `create/${iouType}/start/${transactionID}/${reportID}` as const,
},
MONEY_REQUEST_STEP_CONFIRMATION: {
route: 'create/:iouType/confirmation/:transactionID/:reportID',
@@ -352,9 +349,9 @@ const ROUTES = {
getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_DISTANCE: {
- route: 'create/:iouType/distance/:transactionID/:reportID',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo),
+ route: ':action/:iouType/distance/:transactionID/:reportID',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action}/${iouType}/distance/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_MERCHANT: {
route: ':action/:iouType/merchant/:transactionID/:reportID',
@@ -395,16 +392,19 @@ const ROUTES = {
getRoute: (iouType: ValueOf, iouRequestType: ValueOf) => `start/${iouType}/${iouRequestType}` as const,
},
MONEY_REQUEST_CREATE_TAB_DISTANCE: {
- route: 'create/:iouType/start/:transactionID/:reportID/distance',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/distance` as const,
+ route: ':action/:iouType/start/:transactionID/:reportID/distance',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
+ `create/${iouType}/start/${transactionID}/${reportID}/distance` as const,
},
MONEY_REQUEST_CREATE_TAB_MANUAL: {
- route: 'create/:iouType/start/:transactionID/:reportID/manual',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/manual` as const,
+ route: ':action/:iouType/start/:transactionID/:reportID/manual',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
+ `create/${iouType}/start/${transactionID}/${reportID}/manual` as const,
},
MONEY_REQUEST_CREATE_TAB_SCAN: {
- route: 'create/:iouType/start/:transactionID/:reportID/scan',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/scan` as const,
+ route: ':action/:iouType/start/:transactionID/:reportID/scan',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) =>
+ `create/${iouType}/start/${transactionID}/${reportID}/scan` as const,
},
IOU_REQUEST: 'request/new',
@@ -637,8 +637,16 @@ const ROUTES = {
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates/new` as const,
},
WORKSPACE_DISTANCE_RATES_SETTINGS: {
- route: 'settings/workspace/:policyID/distance-rates/settings',
- getRoute: (policyID: string) => `settings/workspace/${policyID}/distance-rates/settings` as const,
+ route: 'settings/workspaces/:policyID/distance-rates/settings',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates/settings` as const,
+ },
+ WORKSPACE_DISTANCE_RATE_DETAILS: {
+ route: 'settings/workspaces/:policyID/distance-rates/:rateID',
+ getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}` as const,
+ },
+ WORKSPACE_DISTANCE_RATE_EDIT: {
+ route: 'settings/workspaces/:policyID/distance-rates/:rateID/edit',
+ getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/edit` as const,
},
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 4d4e9ea327c6..82fef0383918 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -155,7 +155,6 @@ const SCREENS = {
CURRENCY: 'Money_Request_Currency',
WAYPOINT: 'Money_Request_Waypoint',
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
- DISTANCE: 'Money_Request_Distance',
RECEIPT: 'Money_Request_Receipt',
},
@@ -246,6 +245,8 @@ const SCREENS = {
DISTANCE_RATES: 'Distance_Rates',
CREATE_DISTANCE_RATE: 'Create_Distance_Rate',
DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings',
+ DISTANCE_RATE_DETAILS: 'Distance_Rate_Details',
+ DISTANCE_RATE_EDIT: 'Distance_Rate_Edit',
},
EDIT_REQUEST: {
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
index b934bdfdd738..9524c5203110 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
@@ -15,10 +15,19 @@ import CONST from '@src/CONST';
function extractAttachmentsFromReport(parentReportAction, reportActions) {
const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
const attachments = [];
+ // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate
+ // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position.
+ const uniqueSources = new Set();
const htmlParser = new HtmlParser({
onopentag: (name, attribs) => {
if (name === 'video') {
+ const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]);
+ if (uniqueSources.has(source)) {
+ return;
+ }
+
+ uniqueSources.add(source);
const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/');
attachments.unshift({
reportActionID: null,
@@ -35,7 +44,20 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) {
if (name === 'img' && attribs.src) {
const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src);
- const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);
+ if (uniqueSources.has(source)) {
+ return;
+ }
+
+ uniqueSources.add(source);
+ let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);
+
+ // Public image URLs might lack a file extension in the source URL, without an extension our
+ // AttachmentView fails to recognize them as images and renders fallback content instead.
+ // We apply this small hack to add an image extension and ensure AttachmentView renders the image.
+ const fileInfo = FileUtils.splitExtensionFromFileName(fileName);
+ if (!fileInfo.fileExtension) {
+ fileName = `${fileInfo.fileName || 'image'}.jpg`;
+ }
// By iterating actions in chronological order and prepending each attachment
// we ensure correct order of attachments even across actions with multiple attachments.
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index 461548f0d2b1..9fe37734e8ee 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -79,6 +79,7 @@ const defaultProps = {
reportActionID: '',
isHovered: false,
optionalVideoDuration: 0,
+ fallbackSource: Expensicons.Gallery,
};
function AttachmentView({
@@ -201,6 +202,21 @@ function AttachmentView({
// We also check for numeric source since this is how static images (used for preview) are represented in RN.
const isImage = typeof source === 'number' || Str.isImage(source);
if (isImage || (file && Str.isImage(file.name))) {
+ if (imageError) {
+ // AttachmentViewImage can't handle icon fallbacks, so we need to handle it here
+ if (typeof fallbackSource === 'number' || _.isFunction(fallbackSource)) {
+ return (
+
+ );
+ }
+ }
+
return (
({
success={success}
ref={buttonRef}
pressOnEnter={pressOnEnter}
- isDisabled={isDisabled}
+ isDisabled={isDisabled || !!options[0].disabled}
style={[styles.w100, style]}
isLoading={isLoading}
text={selectedItem.text}
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index 83100788761f..87db9a29d827 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -22,6 +22,7 @@ type DropdownOption = {
iconHeight?: number;
iconDescription?: string;
onSelected?: () => void;
+ disabled?: boolean;
};
type ButtonWithDropdownMenuProps = {
diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx
index 356fbd3726a3..524c8a3903e0 100644
--- a/src/components/CustomStatusBarAndBackground/index.tsx
+++ b/src/components/CustomStatusBarAndBackground/index.tsx
@@ -114,6 +114,11 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
[prevIsRootStatusBarEnabled, isRootStatusBarEnabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle],
);
+ useEffect(() => {
+ updateStatusBarAppearance({backgroundColor: theme.appBG});
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want this to run on first render
+ }, []);
+
useEffect(() => {
didForceUpdateStatusBarRef.current = false;
}, [isRootStatusBarEnabled]);
diff --git a/src/components/DistanceRequest/index.tsx b/src/components/DistanceRequest/index.tsx
deleted file mode 100644
index f9e8c0be12ff..000000000000
--- a/src/components/DistanceRequest/index.tsx
+++ /dev/null
@@ -1,320 +0,0 @@
-import type {RouteProp} from '@react-navigation/native';
-import lodashIsEqual from 'lodash/isEqual';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {View} from 'react-native';
-// eslint-disable-next-line no-restricted-imports
-import type {ScrollView} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import type {OnyxEntry} from 'react-native-onyx';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import Button from '@components/Button';
-import DotIndicatorMessage from '@components/DotIndicatorMessage';
-import DraggableList from '@components/DraggableList';
-import type {DraggableListData} from '@components/DraggableList/types';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useLocalize from '@hooks/useLocalize';
-import useNetwork from '@hooks/useNetwork';
-import usePrevious from '@hooks/usePrevious';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as ErrorUtils from '@libs/ErrorUtils';
-import * as IOUUtils from '@libs/IOUUtils';
-import Navigation from '@libs/Navigation/Navigation';
-import * as TransactionUtils from '@libs/TransactionUtils';
-import * as MapboxToken from '@userActions/MapboxToken';
-import * as TransactionUserActions from '@userActions/Transaction';
-import * as TransactionEdit from '@userActions/TransactionEdit';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import type {Report, Transaction} from '@src/types/onyx';
-import type {WaypointCollection} from '@src/types/onyx/Transaction';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import DistanceRequestFooter from './DistanceRequestFooter';
-import DistanceRequestRenderItem from './DistanceRequestRenderItem';
-
-type DistanceRequestOnyxProps = {
- transaction: OnyxEntry;
-};
-
-type DistanceRequestProps = DistanceRequestOnyxProps & {
- /** The TransactionID of this request */
- transactionID?: string;
-
- /** The report to which the distance request is associated */
- report: OnyxEntry;
-
- /** Are we editing an existing distance request, or creating a new one? */
- isEditingRequest?: boolean;
-
- /** Are we editing the distance while creating a new distance request */
- isEditingNewRequest?: boolean;
-
- /** Called on submit of this page */
- onSubmit: (waypoints?: WaypointCollection) => void;
-
- /** React Navigation route */
- route: RouteProp<{
- /** Params from the route */
- params: {
- /** The type of IOU report, i.e. bill, request, send */
- iouType: string;
- /** The report ID of the IOU */
- reportID: string;
- };
- }>;
-};
-
-function DistanceRequest({transactionID = '', report, transaction, route, isEditingRequest = false, isEditingNewRequest = false, onSubmit}: DistanceRequestProps) {
- const styles = useThemeStyles();
- const {isOffline} = useNetwork();
- const {translate} = useLocalize();
-
- const [optimisticWaypoints, setOptimisticWaypoints] = useState();
- const [hasError, setHasError] = useState(false);
- const reportID = report?.reportID ?? '';
- const waypoints: WaypointCollection = useMemo(() => optimisticWaypoints ?? transaction?.comment?.waypoints ?? {waypoint0: {}, waypoint1: {}}, [optimisticWaypoints, transaction]);
- const waypointsList = Object.keys(waypoints);
- const iouType = route?.params?.iouType ?? '';
- const previousWaypoints = usePrevious(waypoints);
- const numberOfWaypoints = Object.keys(waypoints).length;
- const numberOfPreviousWaypoints = Object.keys(previousWaypoints).length;
- const scrollViewRef = useRef(null);
-
- const isLoadingRoute = transaction?.comment?.isLoading ?? false;
- const isLoading = transaction?.isLoading ?? false;
- const hasRouteError = Boolean(transaction?.errorFields?.route);
- const hasRoute = TransactionUtils.hasRoute((transaction ?? {}) as Transaction);
- const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints);
- const previousValidatedWaypoints = usePrevious(validatedWaypoints);
- const haveValidatedWaypointsChanged = !lodashIsEqual(previousValidatedWaypoints, validatedWaypoints);
- const isRouteAbsentWithoutErrors = !hasRoute && !hasRouteError;
- const shouldFetchRoute = (isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1;
- const transactionWasSaved = useRef(false);
-
- useEffect(() => {
- MapboxToken.init();
- return MapboxToken.stop;
- }, []);
-
- useEffect(() => {
- if (!isEditingNewRequest && !isEditingRequest) {
- return () => {};
- }
- // This effect runs when the component is mounted and unmounted. It's purpose is to be able to properly
- // discard changes if the user cancels out of making any changes. This is accomplished by backing up the
- // original transaction, letting the user modify the current transaction, and then if the user ever
- // cancels out of the modal without saving changes, the original transaction is restored from the backup.
-
- // On mount, create the backup transaction.
- TransactionEdit.createBackupTransaction(transaction);
-
- return () => {
- // If the user cancels out of the modal without without saving changes, then the original transaction
- // needs to be restored from the backup so that all changes are removed.
- if (transactionWasSaved.current) {
- return;
- }
- TransactionEdit.restoreOriginalTransactionFromBackup(transaction?.transactionID ?? '');
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- const transactionWaypoints = transaction?.comment?.waypoints ?? {};
- if (!transaction?.transactionID || Object.keys(transactionWaypoints).length) {
- return;
- }
-
- // Create the initial start and stop waypoints
- TransactionUserActions.createInitialWaypoints(transactionID);
- return () => {
- // Whenever we reset the transaction, we need to set errors as empty/false.
- setHasError(false);
- };
- }, [transaction, transactionID]);
-
- useEffect(() => {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (isOffline || !shouldFetchRoute) {
- return;
- }
-
- TransactionUserActions.getRoute(transactionID, validatedWaypoints);
- }, [shouldFetchRoute, transactionID, validatedWaypoints, isOffline]);
-
- useEffect(() => {
- if (numberOfWaypoints <= numberOfPreviousWaypoints) {
- return;
- }
- scrollViewRef.current?.scrollToEnd({animated: true});
- }, [numberOfPreviousWaypoints, numberOfWaypoints]);
-
- useEffect(() => {
- // Whenever we change waypoints we need to remove the error or it will keep showing the error.
- if (lodashIsEqual(previousWaypoints, waypoints)) {
- return;
- }
- setHasError(false);
- }, [waypoints, previousWaypoints]);
-
- const navigateBack = () => {
- Navigation.goBack(isEditingNewRequest ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME);
- };
-
- /**
- * Takes the user to the page for editing a specific waypoint
- */
- const navigateToWaypointEditPage = (index: number) => {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(
- CONST.IOU.ACTION.EDIT,
- CONST.IOU.TYPE.REQUEST,
- transactionID,
- report?.reportID ?? '',
- index.toString(),
- Navigation.getActiveRouteWithoutParams(),
- ),
- );
- };
-
- const getError = useCallback(() => {
- // Get route error if available else show the invalid number of waypoints error.
- if (hasRouteError) {
- return ErrorUtils.getLatestErrorField((transaction ?? {}) as Transaction, 'route');
- }
-
- if (Object.keys(validatedWaypoints).length < 2) {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- return {0: 'iou.error.atLeastTwoDifferentWaypoints'};
- }
-
- if (Object.keys(validatedWaypoints).length < Object.keys(waypoints).length) {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- return {0: translate('iou.error.duplicateWaypointsErrorMessage')};
- }
- }, [translate, transaction, hasRouteError, validatedWaypoints, waypoints]);
-
- const updateWaypoints = useCallback(
- ({data}: DraggableListData) => {
- if (lodashIsEqual(waypointsList, data)) {
- return;
- }
-
- const newWaypoints: WaypointCollection = {};
- let emptyWaypointIndex = -1;
- data.forEach((waypoint, index) => {
- newWaypoints[`waypoint${index}`] = waypoints?.[waypoint] ?? {};
- // Find waypoint that BECOMES empty after dragging
- if (isEmptyObject(newWaypoints[`waypoint${index}`]) && !isEmptyObject(waypoints[`waypoint${index}`])) {
- emptyWaypointIndex = index;
- }
- });
-
- setOptimisticWaypoints(newWaypoints);
- // eslint-disable-next-line rulesdir/no-thenable-actions-in-views
- Promise.all([TransactionUserActions.removeWaypoint(transaction, emptyWaypointIndex.toString()), TransactionUserActions.updateWaypoints(transactionID, newWaypoints)]).then(() => {
- setOptimisticWaypoints(undefined);
- });
- },
- [transactionID, transaction, waypoints, waypointsList],
- );
-
- const submitWaypoints = useCallback(() => {
- // If there is any error or loading state, don't let user go to next page.
- if (!isEmptyObject(getError()) || isLoadingRoute || (isLoading && !isOffline)) {
- setHasError(true);
- return;
- }
-
- if (isEditingNewRequest || isEditingRequest) {
- transactionWasSaved.current = true;
- }
-
- onSubmit(waypoints);
- }, [onSubmit, setHasError, getError, isLoadingRoute, isLoading, waypoints, isEditingNewRequest, isEditingRequest, isOffline]);
-
- const content = (
- <>
-
- item}
- shouldUsePortal
- onDragEnd={updateWaypoints}
- ref={scrollViewRef}
- renderItem={({item, drag, isActive, getIndex}) => (
- number}
- onPress={navigateToWaypointEditPage}
- disabled={isLoadingRoute}
- />
- )}
- ListFooterComponent={
-
- }
- />
-
-
- {/* Show error message if there is route error or there are less than 2 routes and user has tried submitting, */}
- {((hasError && !isEmptyObject(getError())) || hasRouteError) && (
-
- )}
-
-
- >
- );
-
- if (!isEditingNewRequest) {
- return content;
- }
-
- return (
-
- {({safeAreaPaddingBottomStyle}) => (
-
-
-
- {content}
-
-
- )}
-
- );
-}
-
-DistanceRequest.displayName = 'DistanceRequest';
-export default withOnyx({
- transaction: {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID || 0}`,
- },
-})(DistanceRequest);
diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js
index e138ca4d4194..6bceaf570ccc 100644
--- a/src/components/EmojiPicker/EmojiPicker.js
+++ b/src/components/EmojiPicker/EmojiPicker.js
@@ -7,6 +7,7 @@ import withViewportOffsetTop from '@components/withViewportOffsetTop';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as Browser from '@libs/Browser';
import calculateAnchorPosition from '@libs/calculateAnchorPosition';
import CONST from '@src/CONST';
import EmojiPickerMenu from './EmojiPickerMenu';
@@ -169,6 +170,7 @@ const EmojiPicker = forwardRef((props, ref) => {
// emojis. The best alternative is to set it to 1ms so it just "pops" in and out
return (
{emojiCode};
+}
+
+EmojiWithTooltip.displayName = 'EmojiWithTooltip';
+
+export default EmojiWithTooltip;
diff --git a/src/components/EmojiWithTooltip/index.tsx b/src/components/EmojiWithTooltip/index.tsx
new file mode 100644
index 000000000000..32103544b3aa
--- /dev/null
+++ b/src/components/EmojiWithTooltip/index.tsx
@@ -0,0 +1,42 @@
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import Text from '@components/Text';
+import Tooltip from '@components/Tooltip';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as EmojiUtils from '@libs/EmojiUtils';
+import type EmojiWithTooltipProps from './types';
+
+function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) {
+ const {preferredLocale} = useLocalize();
+ const styles = useThemeStyles();
+ const emoji = EmojiUtils.findEmojiByCode(emojiCode);
+ const emojiName = EmojiUtils.getEmojiName(emoji, preferredLocale);
+
+ const emojiTooltipContent = useCallback(
+ () => (
+
+
+
+ {emojiCode}
+
+
+ {`:${emojiName}:`}
+
+ ),
+ [emojiCode, emojiName, styles.alignItemsCenter, styles.ph2, styles.flexRow, styles.emojiTooltipWrapper, styles.fontColorReactionLabel, styles.onlyEmojisText, styles.textMicro],
+ );
+
+ return (
+
+ {emojiCode}
+
+ );
+}
+
+EmojiWithTooltip.displayName = 'EmojiWithTooltip';
+
+export default EmojiWithTooltip;
diff --git a/src/components/EmojiWithTooltip/types.ts b/src/components/EmojiWithTooltip/types.ts
new file mode 100644
index 000000000000..d13c389c0568
--- /dev/null
+++ b/src/components/EmojiWithTooltip/types.ts
@@ -0,0 +1,8 @@
+import type {StyleProp, TextStyle} from 'react-native';
+
+type EmojiWithTooltipProps = {
+ emojiCode: string;
+ style?: StyleProp;
+};
+
+export default EmojiWithTooltipProps;
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
index bd4f72c63ec3..af04c11de41e 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
@@ -70,6 +70,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.block,
}),
+ emoji: HTMLElementModel.fromCustomModel({tagName: 'emoji', contentModel: HTMLContentModel.textual}),
}),
[styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16],
);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx
new file mode 100644
index 000000000000..6582e99477a8
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
+import EmojiWithTooltip from '@components/EmojiWithTooltip';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+function EmojiRenderer({tnode}: CustomRendererProps) {
+ const styles = useThemeStyles();
+ const style = 'islarge' in tnode.attributes ? styles.onlyEmojisText : {};
+ return (
+
+ );
+}
+
+EmojiRenderer.displayName = 'EmojiRenderer';
+
+export default EmojiRenderer;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
index 1914bcf4b5ff..fdd0c89ec5a0 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
@@ -2,6 +2,7 @@ import type {CustomTagRendererRecord} from 'react-native-render-html';
import AnchorRenderer from './AnchorRenderer';
import CodeRenderer from './CodeRenderer';
import EditedRenderer from './EditedRenderer';
+import EmojiRenderer from './EmojiRenderer';
import ImageRenderer from './ImageRenderer';
import MentionHereRenderer from './MentionHereRenderer';
import MentionUserRenderer from './MentionUserRenderer';
@@ -25,6 +26,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
/* eslint-disable @typescript-eslint/naming-convention */
'mention-user': MentionUserRenderer,
'mention-here': MentionHereRenderer,
+ emoji: EmojiRenderer,
'next-step-email': NextStepEmailRenderer,
/* eslint-enable @typescript-eslint/naming-convention */
};
diff --git a/src/components/InlineCodeBlock/getCurrentData.ts b/src/components/InlineCodeBlock/getCurrentData.ts
new file mode 100644
index 000000000000..591ec74c885d
--- /dev/null
+++ b/src/components/InlineCodeBlock/getCurrentData.ts
@@ -0,0 +1,11 @@
+import type {TDefaultRendererProps} from 'react-native-render-html';
+import type {TTextOrTPhrasing} from './types';
+
+// Create a temporary solution to display when there are emojis in the inline code block
+// We can remove this after https://github.com/Expensify/App/issues/14676 is fixed
+export default function getCurrentData(defaultRendererProps: TDefaultRendererProps): string {
+ if ('data' in defaultRendererProps.tnode) {
+ return defaultRendererProps.tnode.data;
+ }
+ return defaultRendererProps.tnode.children.map((child) => ('data' in child ? child.data : '')).join('');
+}
diff --git a/src/components/InlineCodeBlock/index.native.tsx b/src/components/InlineCodeBlock/index.native.tsx
index 85d02b7239ca..1c8a1bea4312 100644
--- a/src/components/InlineCodeBlock/index.native.tsx
+++ b/src/components/InlineCodeBlock/index.native.tsx
@@ -1,11 +1,13 @@
import React from 'react';
import useThemeStyles from '@hooks/useThemeStyles';
+import getCurrentData from './getCurrentData';
import type InlineCodeBlockProps from './types';
import type {TTextOrTPhrasing} from './types';
import WrappedText from './WrappedText';
function InlineCodeBlock({TDefaultRenderer, defaultRendererProps, textStyle, boxModelStyle}: InlineCodeBlockProps) {
const styles = useThemeStyles();
+ const data = getCurrentData(defaultRendererProps);
return (
({TDefaultRenderer,
textStyles={textStyle}
wordStyles={[boxModelStyle, styles.codeWordStyle]}
>
- {'data' in defaultRendererProps.tnode && defaultRendererProps.tnode.data}
+ {data}
);
diff --git a/src/components/InlineCodeBlock/index.tsx b/src/components/InlineCodeBlock/index.tsx
index 593a08aaad5e..26a4e8b7a31f 100644
--- a/src/components/InlineCodeBlock/index.tsx
+++ b/src/components/InlineCodeBlock/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {StyleSheet} from 'react-native';
import Text from '@components/Text';
+import getCurrentData from './getCurrentData';
import type InlineCodeBlockProps from './types';
import type {TTextOrTPhrasing} from './types';
@@ -8,12 +9,14 @@ function InlineCodeBlock({TDefaultRenderer,
const flattenTextStyle = StyleSheet.flatten(textStyle);
const {textDecorationLine, ...textStyles} = flattenTextStyle;
+ const data = getCurrentData(defaultRendererProps);
+
return (
- {'data' in defaultRendererProps.tnode && defaultRendererProps.tnode.data}
+ {data}
);
}
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index fa4c89216d08..07a2cb4b71ee 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -109,7 +109,7 @@ function LHNOptionsList({
],
);
- const extraData = useMemo(() => [reportActions, reports, policy, personalDetails], [reportActions, reports, policy, personalDetails]);
+ const extraData = useMemo(() => [reportActions, reports, policy, personalDetails, data.length], [reportActions, reports, policy, personalDetails, data.length]);
return (
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index 0db8e581e23e..121390d808b5 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -37,7 +37,7 @@ function OptionRowLHNData({
const optionItemRef = useRef();
- const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null);
+ const shouldDisplayViolations = canUseViolations && ReportUtils.shouldDisplayTransactionThreadViolations(fullReport, transactionViolations, parentReportAction ?? null);
const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
@@ -48,7 +48,7 @@ function OptionRowLHNData({
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
- hasViolations: !!hasViolations,
+ hasViolations: !!shouldDisplayViolations,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
index 71c0fe47ffca..76f4b251ec83 100644
--- a/src/components/Modal/index.tsx
+++ b/src/components/Modal/index.tsx
@@ -1,4 +1,4 @@
-import React, {useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import StatusBar from '@libs/StatusBar';
@@ -6,7 +6,7 @@ import CONST from '@src/CONST';
import BaseModal from './BaseModal';
import type BaseModalProps from './types';
-function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = () => {}, children, ...rest}: BaseModalProps) {
+function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = () => {}, children, shouldHandleNavigationBack, ...rest}: BaseModalProps) {
const theme = useTheme();
const StyleUtils = useStyleUtils();
const [previousStatusBarColor, setPreviousStatusBarColor] = useState();
@@ -22,8 +22,15 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
const hideModal = () => {
setStatusBarColor(previousStatusBarColor);
onModalHide();
+ if (window.history.state.shouldGoBack) {
+ window.history.back();
+ }
};
+ const handlePopStateRef = useRef(() => {
+ rest.onClose();
+ });
+
const showModal = () => {
const statusBarColor = StatusBar.getBackgroundColor() ?? theme.appBG;
@@ -35,9 +42,20 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
setStatusBarColor(isFullScreenModal ? theme.appBG : StyleUtils.getThemeBackgroundColor(statusBarColor));
}
+ if (shouldHandleNavigationBack) {
+ window.history.pushState({shouldGoBack: true}, '', null);
+ window.addEventListener('popstate', handlePopStateRef.current);
+ }
onModalShow?.();
};
+ useEffect(
+ () => () => {
+ window.removeEventListener('popstate', handlePopStateRef.current);
+ },
+ [],
+ );
+
return (
& {
* */
hideModalContentWhileAnimating?: boolean;
+ /** Whether handle navigation back when modal show. */
+ shouldHandleNavigationBack?: boolean;
+
/** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */
shouldUseCustomBackdrop?: boolean;
};
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 2bf346ec8de4..9c3d9b8640e7 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -67,9 +67,12 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, chatReport, policy), [moneyRequestReport, chatReport, policy]);
+ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);
+
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0;
+ const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep;
@@ -121,6 +124,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
+ shouldDisableApproveButton={shouldDisableApproveButton}
style={[styles.pv2]}
formattedAmount={formattedAmount}
isDisabled={!canAllowSettlement}
@@ -135,6 +139,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
text={translate('common.submit')}
style={[styles.mnw120, styles.pv2, styles.pr0]}
onPress={() => IOU.submitReport(moneyRequestReport)}
+ isDisabled={shouldDisableSubmitButton}
/>
)}
@@ -153,6 +158,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
+ shouldDisableApproveButton={shouldDisableApproveButton}
formattedAmount={formattedAmount}
isDisabled={!canAllowSettlement}
/>
@@ -166,6 +172,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
text={translate('common.submit')}
style={[styles.w100, styles.pr0]}
onPress={() => IOU.submitReport(moneyRequestReport)}
+ isDisabled={shouldDisableSubmitButton}
/>
)}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index a4da7e551515..4550a7aef5d2 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -721,7 +721,17 @@ function MoneyRequestConfirmationList({
description={translate('common.distance')}
style={styles.moneyRequestMenuItem}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))}
+ onPress={() =>
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
+ )
+ }
disabled={didConfirm || !canEditDistance}
interactive={!isReadOnly}
/>
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx
index cd3b1951ceb7..b740e0aa0834 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx
@@ -267,7 +267,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const navigateBack = () => {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, transaction?.transactionID ?? '', reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID));
};
const shouldDisplayFieldError: boolean = useMemo(() => {
@@ -693,7 +693,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()))
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
+ )
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={didConfirm || !canEditDistance}
diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx
index e1cd18ba4767..19fc86c9f936 100644
--- a/src/components/Popover/index.tsx
+++ b/src/components/Popover/index.tsx
@@ -92,6 +92,7 @@ function Popover(props: PopoverProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onClose={onCloseWithPopoverContext}
+ shouldHandleNavigationBack={props.shouldHandleNavigationBack}
type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.POPOVER}
popoverAnchorPosition={isSmallScreenWidth ? undefined : anchorPosition}
fullscreen={isSmallScreenWidth ? true : fullscreen}
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index 314c1ba141c3..4e2f38293f6e 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -37,6 +37,9 @@ type PopoverProps = BaseModalProps &
/** Whether we want to show the popover on the right side of the screen */
fromSidebarMediumScreen?: boolean;
+
+ /** Whether handle navigation back when modal show. */
+ shouldHandleNavigationBack?: boolean;
};
type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps;
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 44a446b56653..8f54de5182f8 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -24,6 +24,9 @@ type PopoverMenuItem = MenuItemProps & {
/** Sub menu items to be rendered after a menu item is selected */
subMenuItems?: PopoverMenuItem[];
+
+ /** Determines whether the menu item is disabled or not */
+ disabled?: boolean;
};
type PopoverModalProps = Pick;
@@ -205,6 +208,7 @@ function PopoverMenu({
displayInDefaultIconColor={item.displayInDefaultIconColor}
shouldShowRightIcon={item.shouldShowRightIcon}
shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
+ disabled={item.disabled}
/>
))}
diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx
index 792002441ac6..deda6dbd217a 100644
--- a/src/components/PopoverWithMeasuredContent.tsx
+++ b/src/components/PopoverWithMeasuredContent.tsx
@@ -14,6 +14,9 @@ import type {WindowDimensionsProps} from './withWindowDimensions/types';
type PopoverWithMeasuredContentProps = Omit & {
/** The horizontal and vertical anchors points for the popover */
anchorPosition: AnchorPosition;
+
+ /** Whether handle navigation back when modal show. */
+ shouldHandleNavigationBack?: boolean;
};
/**
@@ -42,6 +45,7 @@ function PopoverWithMeasuredContent({
statusBarTranslucent = true,
avoidKeyboard = false,
hideModalContentWhileAnimating = false,
+ shouldHandleNavigationBack = false,
...props
}: PopoverWithMeasuredContentProps) {
const styles = useThemeStyles();
@@ -117,6 +121,7 @@ function PopoverWithMeasuredContent({
};
return isContentMeasured ? (
Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))}
+ onPress={() =>
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID),
+ )
+ }
/>
) : (
diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx
index ffc12957dcb4..c66dc36d1ed5 100644
--- a/src/components/ReportActionItem/ReportActionItemImages.tsx
+++ b/src/components/ReportActionItem/ReportActionItemImages.tsx
@@ -63,45 +63,45 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report
const triangleWidth = variables.reportActionItemImagesMoreCornerTriangleWidth;
return (
-
- {shownImages.map(({thumbnail, image, transaction, isLocalFile, filename}, index) => {
- const isLastImage = index === numberOfShownImages - 1;
-
- // Show a border to separate multiple images. Shown to the right for each except the last.
- const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1;
- const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {};
- return (
-
+
+ {shownImages.map(({thumbnail, image, transaction, isLocalFile, filename}, index) => {
+ // Show a border to separate multiple images. Shown to the right for each except the last.
+ const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1;
+ const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {};
+ return (
+
+
+
+ );
+ })}
+
+ {remaining > 0 && (
+
+
+
- );
- })}
+
+ {remaining > MAX_REMAINING ? `${MAX_REMAINING}+` : `+${remaining}`}
+
+ )}
);
}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index d183d27fefb8..b843443be4af 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -153,6 +153,7 @@ function ReportPreview({
});
const shouldShowSubmitButton = isOpenExpenseReport && reimbursableSpend !== 0;
+ const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport);
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
@@ -209,6 +210,8 @@ function ReportPreview({
const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, chatReport, policy), [iouReport, chatReport, policy]);
+ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport);
+
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID);
@@ -307,6 +310,7 @@ function ReportPreview({
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
+ shouldDisableApproveButton={shouldDisableApproveButton}
kycWallAnchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
@@ -324,6 +328,7 @@ function ReportPreview({
success={isWaitingForSubmissionFromCurrentUser}
text={translate('common.submit')}
onPress={() => iouReport && IOU.submitReport(iouReport)}
+ isDisabled={shouldDisableSubmitButton}
/>
)}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index fac78ee786a0..e691a5bdb191 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -107,6 +107,9 @@ type ListItem = {
/** Whether to wrap long text up to 2 lines */
isMultilineSupported?: boolean;
+
+ /** The search value from the selection list */
+ searchText?: string | null;
};
type ListItemProps = CommonListItemProps & {
diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx
index 6b6ad3af737a..0ea8ea308d6a 100644
--- a/src/components/SettlementButton.tsx
+++ b/src/components/SettlementButton.tsx
@@ -60,6 +60,9 @@ type SettlementButtonProps = SettlementButtonOnyxProps & {
/** Should we show the payment options? */
shouldShowApproveButton?: boolean;
+ /** Should approve button be disabled? */
+ shouldDisableApproveButton?: boolean;
+
/** The policyID of the report we are paying */
policyID?: string;
@@ -124,6 +127,7 @@ function SettlementButton({
policyID = '',
shouldHidePaymentOptions = false,
shouldShowApproveButton = false,
+ shouldDisableApproveButton = false,
style,
shouldShowPersonalBankAccountOption = false,
enterKeyEventListenerPriority = 0,
@@ -166,6 +170,7 @@ function SettlementButton({
text: translate('iou.approve'),
icon: Expensicons.ThumbsUp,
value: CONST.IOU.REPORT_ACTION_TYPE.APPROVE,
+ disabled: !!shouldDisableApproveButton,
};
const canUseWallet = !isExpenseReport && currency === CONST.CURRENCY.USD;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 29618b083bd5..c3ad6d82d6b2 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -20,13 +20,13 @@ import type {
DeleteActionParams,
DeleteConfirmationParams,
DidSplitAmountMessageParams,
+ DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
EnterMagicCodeParams,
FormattedMaxLengthParams,
GoBackMessageParams,
GoToRoomParams,
- HeldRequestParams,
InstantSummaryParams,
LocalTimeParams,
LoggedInAsParams,
@@ -691,7 +691,7 @@ export default {
hold: 'Hold',
holdRequest: 'Hold request',
unholdRequest: 'Unhold request',
- heldRequest: ({comment}: HeldRequestParams) => `held this request with the comment: ${comment}`,
+ heldRequest: 'held this request',
unheldRequest: 'unheld this request',
explainHold: "Explain why you're holding this request.",
reason: 'Reason',
@@ -2036,17 +2036,17 @@ export default {
centrallyManage: 'Centrally manage rates, choose to track in miles or kilometers, and set a default category.',
rate: 'Rate',
addRate: 'Add rate',
- deleteRate: 'Delete rate',
- deleteRates: 'Delete rates',
+ deleteRates: ({count}: DistanceRateOperationsParams) => `Delete ${Str.pluralize('rate', 'rates', count)}`,
+ enableRates: ({count}: DistanceRateOperationsParams) => `Enable ${Str.pluralize('rate', 'rates', count)}`,
+ disableRates: ({count}: DistanceRateOperationsParams) => `Disable ${Str.pluralize('rate', 'rates', count)}`,
enableRate: 'Enable rate',
- disableRate: 'Disable rate',
- disableRates: 'Disable rates',
- enableRates: 'Enable rates',
status: 'Status',
enabled: 'Enabled',
disabled: 'Disabled',
unit: 'Unit',
defaultCategory: 'Default category',
+ deleteDistanceRate: 'Delete distance rate',
+ areYouSureDelete: ({count}: DistanceRateOperationsParams) => `Are you sure you want to delete ${Str.pluralize('this rate', 'these rates', count)}?`,
},
editor: {
descriptionInputLabel: 'Description',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 874921ee911a..78b80adb16d4 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -18,6 +18,7 @@ import type {
DeleteActionParams,
DeleteConfirmationParams,
DidSplitAmountMessageParams,
+ DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
EnglishTranslation,
@@ -25,7 +26,6 @@ import type {
FormattedMaxLengthParams,
GoBackMessageParams,
GoToRoomParams,
- HeldRequestParams,
InstantSummaryParams,
LocalTimeParams,
LoggedInAsParams,
@@ -688,7 +688,7 @@ export default {
enableWallet: 'Habilitar Billetera',
holdRequest: 'Bloquear solicitud',
unholdRequest: 'Desbloquear solicitud',
- heldRequest: ({comment}: HeldRequestParams) => `bloqueó esta solicitud con el comentario: ${comment}`,
+ heldRequest: 'bloqueó esta solicitud',
unheldRequest: 'desbloqueó esta solicitud',
explainHold: 'Explica la razón para bloquear esta solicitud.',
reason: 'Razón',
@@ -2064,17 +2064,17 @@ export default {
centrallyManage: 'Gestiona centralizadamente las tasas, elige si contabilizar en millas o kilómetros, y define una categoría por defecto',
rate: 'Tasa',
addRate: 'Agregar tasa',
- deleteRate: 'Eliminar tasa',
- deleteRates: 'Eliminar tasas',
+ deleteRates: ({count}: DistanceRateOperationsParams) => `Eliminar ${Str.pluralize('tasa', 'tasas', count)}`,
+ enableRates: ({count}: DistanceRateOperationsParams) => `Activar ${Str.pluralize('tasa', 'tasas', count)}`,
+ disableRates: ({count}: DistanceRateOperationsParams) => `Desactivar ${Str.pluralize('tasa', 'tasas', count)}`,
enableRate: 'Activar tasa',
- disableRate: 'Desactivar tasa',
- disableRates: 'Desactivar tasas',
- enableRates: 'Activar tasas',
status: 'Estado',
enabled: 'Activada',
disabled: 'Desactivada',
unit: 'Unidad',
defaultCategory: 'Categoría predeterminada',
+ deleteDistanceRate: 'Eliminar tasa de distancia',
+ areYouSureDelete: ({count}: DistanceRateOperationsParams) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta tasa', 'estas tasas', count)}?`,
},
editor: {
nameInputLabel: 'Nombre',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 93438d76885d..c365363f84af 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -295,6 +295,8 @@ type LogSizeParams = {size: number};
type HeldRequestParams = {comment: string};
+type DistanceRateOperationsParams = {count: number};
+
export type {
AdminCanceledRequestParams,
ApprovedAmountParams,
@@ -313,6 +315,7 @@ export type {
DeleteActionParams,
DeleteConfirmationParams,
DidSplitAmountMessageParams,
+ DistanceRateOperationsParams,
EditActionParams,
ElectronicFundsParams,
EnglishTranslation,
diff --git a/src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts b/src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts
new file mode 100644
index 000000000000..d4f972ff9757
--- /dev/null
+++ b/src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts
@@ -0,0 +1,7 @@
+type DeletePolicyDistanceRatesParams = {
+ policyID: string;
+ customUnitID: string;
+ customUnitRateID: string[];
+};
+
+export default DeletePolicyDistanceRatesParams;
diff --git a/src/libs/API/parameters/GetRouteForDraftParams.ts b/src/libs/API/parameters/GetRouteForDraftParams.ts
deleted file mode 100644
index 5a213c3f2d49..000000000000
--- a/src/libs/API/parameters/GetRouteForDraftParams.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-type GetRouteForDraftParams = {
- transactionID: string;
- waypoints: string;
-};
-
-export default GetRouteForDraftParams;
diff --git a/src/libs/API/parameters/HoldMoneyRequestParams.ts b/src/libs/API/parameters/HoldMoneyRequestParams.ts
index 93cb1bd6c524..357194d7ae56 100644
--- a/src/libs/API/parameters/HoldMoneyRequestParams.ts
+++ b/src/libs/API/parameters/HoldMoneyRequestParams.ts
@@ -2,6 +2,7 @@ type HoldMoneyRequestParams = {
transactionID: string;
comment: string;
reportActionID: string;
+ commentReportActionID: string;
};
export default HoldMoneyRequestParams;
diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts
new file mode 100644
index 000000000000..95f5e61448d4
--- /dev/null
+++ b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts
@@ -0,0 +1,7 @@
+type SetPolicyDistanceRatesEnabledParams = {
+ policyID: string;
+ customUnitID: string;
+ customUnitRateArray: string;
+};
+
+export default SetPolicyDistanceRatesEnabledParams;
diff --git a/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts
new file mode 100644
index 000000000000..c16487b3da60
--- /dev/null
+++ b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts
@@ -0,0 +1,7 @@
+type UpdatePolicyDistanceRateValueParams = {
+ policyID: string;
+ customUnitID: string;
+ customUnitRateArray: string;
+};
+
+export default UpdatePolicyDistanceRateValueParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 0049489f1fc2..1895c2426e1a 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -20,7 +20,6 @@ export type {default as GetMissingOnyxMessagesParams} from './GetMissingOnyxMess
export type {default as GetNewerActionsParams} from './GetNewerActionsParams';
export type {default as GetOlderActionsParams} from './GetOlderActionsParams';
export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams';
-export type {default as GetRouteForDraftParams} from './GetRouteForDraftParams';
export type {default as GetRouteParams} from './GetRouteParams';
export type {default as GetStatementPDFParams} from './GetStatementPDFParams';
export type {default as HandleRestrictedEventParams} from './HandleRestrictedEventParams';
@@ -184,6 +183,9 @@ export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMore
export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams';
export type {default as SetPolicyDistanceRatesUnitParams} from './SetPolicyDistanceRatesUnitParams';
export type {default as SetPolicyDistanceRatesDefaultCategoryParams} from './SetPolicyDistanceRatesDefaultCategoryParams';
+export type {default as UpdatePolicyDistanceRateValueParams} from './UpdatePolicyDistanceRateValueParams';
+export type {default as SetPolicyDistanceRatesEnabledParams} from './SetPolicyDistanceRatesEnabledParams';
+export type {default as DeletePolicyDistanceRatesParams} from './DeletePolicyDistanceRatesParams';
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 b83ffd89dcb1..9d6e6b3929b8 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -190,6 +190,9 @@ const WRITE_COMMANDS = {
CREATE_POLICY_DISTANCE_RATE: 'CreatePolicyDistanceRate',
SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit',
SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory',
+ UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue',
+ SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled',
+ DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates',
} as const;
type WriteCommand = ValueOf;
@@ -378,6 +381,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_POLICY_TAX]: Parameters.RenamePolicyTaxParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
+ [WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams;
+ [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams;
+ [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams;
};
const READ_COMMANDS = {
@@ -437,7 +443,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.SEARCH_FOR_REPORTS]: Parameters.SearchForReportsParams;
[READ_COMMANDS.SEND_PERFORMANCE_TIMING]: Parameters.SendPerformanceTimingParams;
[READ_COMMANDS.GET_ROUTE]: Parameters.GetRouteParams;
- [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: Parameters.GetRouteForDraftParams;
+ [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: Parameters.GetRouteParams;
[READ_COMMANDS.GET_STATEMENT_PDF]: Parameters.GetStatementPDFParams;
[READ_COMMANDS.OPEN_ONFIDO_FLOW]: EmptyObject;
[READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: EmptyObject;
diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts
index 29781e718c6f..05f6fbd17503 100644
--- a/src/libs/EmojiUtils.ts
+++ b/src/libs/EmojiUtils.ts
@@ -37,7 +37,10 @@ const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name];
const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code];
-const getEmojiName = (emoji: Emoji, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): string => {
+const getEmojiName = (emoji: Emoji, lang: Locale = CONST.LOCALES.DEFAULT): string => {
+ if (!emoji) {
+ return '';
+ }
if (lang === CONST.LOCALES.DEFAULT) {
return emoji.name;
}
diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts
index 58a82de3df53..a18de0fdcbbf 100644
--- a/src/libs/GroupChatUtils.ts
+++ b/src/libs/GroupChatUtils.ts
@@ -6,8 +6,11 @@ import * as ReportUtils from './ReportUtils';
/**
* Returns the report name if the report is a group chat
*/
-function getGroupChatName(report: OnyxEntry): string | undefined {
- const participants = report?.participantAccountIDs ?? [];
+function getGroupChatName(report: OnyxEntry, shouldApplyLimit = false): string | undefined {
+ let participants = report?.participantAccountIDs ?? [];
+ if (shouldApplyLimit) {
+ participants = participants.slice(0, 5);
+ }
const isMultipleParticipantReport = participants.length > 1;
return participants
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index 07bb22f43b31..65390982f18c 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -11,13 +11,13 @@ function navigateToStartMoneyRequestStep(requestType: ValueOf(desiredLanguage: 'en' | 'es' |
const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es';
const translatedPhrase = getTranslatedPhrase(language, phraseKey, languageAbbreviation, ...phraseParameters);
- if (translatedPhrase) {
+ if (translatedPhrase !== null && translatedPhrase !== undefined) {
return translatedPhrase;
}
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 5835558e5de4..463dcfcd9e99 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -146,6 +146,7 @@ const modalScreenListeners = {
// Clear search input (WorkspaceInvitePage) when modal is closed
SearchInputManager.searchInput = '';
Modal.setModalVisibility(false);
+ Modal.willAlertModalBecomeVisible(false);
},
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index bd5bfc46134a..bc14f346c3f9 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -103,7 +103,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/MoneyRequestWaypointPage').default as React.ComponentType,
- [SCREENS.MONEY_REQUEST.DISTANCE]: () => require('../../../pages/iou/NewDistanceRequestPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../pages/EditRequestReceiptPage').default as React.ComponentType,
});
@@ -265,6 +264,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/categories/EditCategoryPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../pages/workspace/distanceRates/CreateDistanceRatePage').default as React.ComponentType,
[SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRateDetailsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRateEditPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAG_SETTINGS]: () => require('../../../pages/workspace/tags/TagSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index c55145a5d580..d57d0272738b 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -94,7 +94,7 @@ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number
function parseHybridAppUrl(url: HybridAppRoute | Route): Route {
switch (url) {
case HYBRID_APP_ROUTES.MONEY_REQUEST_CREATE:
- return ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.REQUEST, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, ReportUtils.generateReportID());
+ return ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.REQUEST, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, ReportUtils.generateReportID());
default:
return url;
}
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 17f5049aab91..35b129e8b0c0 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -24,7 +24,12 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE, SCREENS.WORKSPACE.TAG_SETTINGS, SCREENS.WORKSPACE.TAG_EDIT],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.WORKSPACE.CATEGORY_EDIT],
- [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS],
+ [SCREENS.WORKSPACE.DISTANCE_RATES]: [
+ SCREENS.WORKSPACE.CREATE_DISTANCE_RATE,
+ SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS,
+ SCREENS.WORKSPACE.DISTANCE_RATE_EDIT,
+ SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS,
+ ],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 130fdf23732f..b8b9280bc576 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -295,6 +295,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: {
path: ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.route,
},
+ [SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: {
+ path: ROUTES.WORKSPACE_DISTANCE_RATE_DETAILS.route,
+ },
+ [SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: {
+ path: ROUTES.WORKSPACE_DISTANCE_RATE_EDIT.route,
+ },
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
path: ROUTES.WORKSPACE_TAGS_SETTINGS.route,
},
@@ -510,7 +516,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route,
[SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route,
[SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route,
- [SCREENS.MONEY_REQUEST.DISTANCE]: ROUTES.MONEY_REQUEST_DISTANCE.route,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT,
[SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 9b0d9ce4decc..3f85aec3a560 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -182,6 +182,14 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.TAG_CREATE]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: {
+ policyID: string;
+ rateID: string;
+ };
+ [SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: {
+ policyID: string;
+ rateID: string;
+ };
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
policyID: string;
};
@@ -376,6 +384,7 @@ type MoneyRequestNavigatorParamList = {
backTo: Routes | undefined;
action: ValueOf;
pageIndex: string;
+ transactionID: string;
};
[SCREENS.MONEY_REQUEST.STEP_MERCHANT]: {
action: ValueOf;
@@ -393,9 +402,12 @@ type MoneyRequestNavigatorParamList = {
waypointIndex: string;
threadReportID: number;
};
- [SCREENS.MONEY_REQUEST.DISTANCE]: {
+ [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: {
+ action: string;
iouType: ValueOf;
+ transactionID: string;
reportID: string;
+ backTo: string;
};
[SCREENS.MONEY_REQUEST.RECEIPT]: {
iouType: string;
diff --git a/src/libs/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts
index 8f1438b1d092..cdcfb13eeb72 100644
--- a/src/libs/PolicyDistanceRatesUtils.ts
+++ b/src/libs/PolicyDistanceRatesUtils.ts
@@ -5,7 +5,7 @@ import getPermittedDecimalSeparator from './getPermittedDecimalSeparator';
import * as MoneyRequestUtils from './MoneyRequestUtils';
import * as NumberUtils from './NumberUtils';
-type RateValueForm = typeof ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM | typeof ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM;
+type RateValueForm = typeof ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM | typeof ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM | typeof ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM;
function validateRateValue(values: FormOnyxValues, currency: string, toLocaleDigit: (arg: string) => string): FormInputErrors {
const errors: FormInputErrors = {};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 39e6c8932aad..7717513d3f59 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -240,7 +240,7 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean {
* Note: Free policies have "instant" submit always enabled.
*/
function isInstantSubmitEnabled(policy: OnyxEntry | EmptyObject): boolean {
- return policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT || policy?.type === CONST.POLICY.TYPE.FREE;
+ return policy?.type === CONST.POLICY.TYPE.FREE || (policy?.autoReporting === true && policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT);
}
/**
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 41b49ff6e476..d0ce53a0e10b 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -3787,7 +3787,7 @@ function buildOptimisticRenamedRoomReportAction(newName: string, oldName: string
* Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically
* @param [created] - Action created time
*/
-function buildOptimisticHoldReportAction(comment: string, created = DateUtils.getDBTime()): OptimisticHoldReportAction {
+function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): OptimisticHoldReportAction {
return {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.HOLD,
@@ -3797,10 +3797,37 @@ function buildOptimisticHoldReportAction(comment: string, created = DateUtils.ge
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
- text: Localize.translateLocal('iou.heldRequest', {comment}),
+ text: Localize.translateLocal('iou.heldRequest'),
},
+ ],
+ person: [
{
- type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ style: 'strong',
+ text: getCurrentUserDisplayNameOrEmail(),
+ },
+ ],
+ automatic: false,
+ avatar: getCurrentUserAvatarOrDefault(),
+ created,
+ shouldShow: true,
+ };
+}
+
+/**
+ * Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically
+ * @param [created] - Action created time
+ */
+function buildOptimisticHoldReportActionComment(comment: string, created = DateUtils.getDBTime()): OptimisticHoldReportAction {
+ return {
+ reportActionID: NumberUtils.rand64(),
+ actionName: CONST.REPORT.ACTIONS.TYPE.HOLDCOMMENT,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ actorAccountID: currentUserAccountID,
+ message: [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ style: 'normal',
text: comment,
},
],
@@ -4250,6 +4277,21 @@ function doesTransactionThreadHaveViolations(report: OnyxEntry, transact
return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations);
}
+/**
+ * Checks if we should display violation - we display violations when the money request has violation and it is not settled
+ */
+function shouldDisplayTransactionThreadViolations(
+ report: OnyxEntry,
+ transactionViolations: OnyxCollection,
+ parentReportAction: OnyxEntry,
+): boolean {
+ const {IOUReportID} = (parentReportAction?.originalMessage as IOUMessage) ?? {};
+ if (isSettled(IOUReportID)) {
+ return false;
+ }
+ return doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
+}
+
/**
* Checks to see if a report contains a violation
*/
@@ -5406,6 +5448,22 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry, approverAccountID?: number): boolean {
+ const policy = getPolicy(report?.policyID);
+ const {preventSelfApproval} = policy;
+
+ const isOwner = (approverAccountID ?? currentUserAccountID) === report?.ownerAccountID;
+
+ return !(preventSelfApproval && isOwner);
+}
+
+function isAllowedToSubmitDraftExpenseReport(report: OnyxEntry): boolean {
+ const policy = getPolicy(report?.policyID);
+ const {submitsTo} = policy;
+
+ return isAllowedToApproveExpenseReport(report, submitsTo);
+}
+
/**
* What missing payment method does this report action indicate, if any?
*/
@@ -5626,6 +5684,7 @@ export {
getRoom,
canEditReportDescription,
doesTransactionThreadHaveViolations,
+ shouldDisplayTransactionThreadViolations,
hasViolations,
navigateToPrivateNotes,
canEditWriteCapability,
@@ -5633,6 +5692,7 @@ export {
hasSmartscanError,
shouldAutoFocusOnKeyPress,
buildOptimisticHoldReportAction,
+ buildOptimisticHoldReportActionComment,
buildOptimisticUnHoldReportAction,
shouldDisplayThreadReplies,
shouldDisableThread,
@@ -5656,6 +5716,8 @@ export {
canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
+ isAllowedToSubmitDraftExpenseReport,
+ isAllowedToApproveExpenseReport,
findSelfDMReportID,
getIndicatedMissingPaymentMethod,
isJoinRequestInAdminRoom,
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index e1d87fa4a292..857f6f39173a 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -36,7 +36,6 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import * as Localize from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
import * as NextStepUtils from '@libs/NextStepUtils';
-import * as NumberUtils from '@libs/NumberUtils';
import Permissions from '@libs/Permissions';
import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -326,7 +325,7 @@ function updateMoneyRequestTypeParams(routes: StackNavigationState, reportID: string) {
clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID);
- Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID));
}
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -4934,7 +4933,7 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On
const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport);
const participants: Participant[] =
ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport
- ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}]
+ ? [{accountID: 0, 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});
@@ -4972,15 +4971,6 @@ function setShownHoldUseExplanation() {
Onyx.set(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, true);
}
-function setUpDistanceTransaction() {
- const transactionID = NumberUtils.rand64();
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
- transactionID,
- comment: {type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, customUnit: {name: CONST.CUSTOM_UNITS.NAME_DISTANCE}},
- });
- Onyx.merge(ONYXKEYS.IOU, {transactionID});
-}
-
/** Navigates to the next IOU page based on where the IOU request was started */
function navigateToNextPage(iou: OnyxEntry, iouType: string, report?: OnyxTypes.Report, path = '') {
const moneyRequestID = `${iouType}${report?.reportID ?? ''}`;
@@ -5031,7 +5021,8 @@ function getIOUReportID(iou?: OnyxTypes.IOU, route?: MoneyRequestRoute): string
* Put money request on HOLD
*/
function putOnHold(transactionID: string, comment: string, reportID: string) {
- const createdReportAction = ReportUtils.buildOptimisticHoldReportAction(comment);
+ const createdReportAction = ReportUtils.buildOptimisticHoldReportAction();
+ const createdReportActionComment = ReportUtils.buildOptimisticHoldReportActionComment(comment);
const optimisticData: OnyxUpdate[] = [
{
@@ -5039,6 +5030,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string) {
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[createdReportAction.reportActionID]: createdReportAction as ReportAction,
+ [createdReportActionComment.reportActionID]: createdReportActionComment as ReportAction,
},
},
{
@@ -5072,6 +5064,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string) {
transactionID,
comment,
reportActionID: createdReportAction.reportActionID,
+ commentReportActionID: createdReportActionComment.reportActionID,
},
{optimisticData, successData, failureData},
);
@@ -5194,7 +5187,6 @@ export {
setMoneyRequestTag,
setMoneyRequestTaxAmount,
setMoneyRequestTaxRate,
- setUpDistanceTransaction,
setShownHoldUseExplanation,
navigateToNextPage,
updateMoneyRequestDate,
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index efd627e7ef93..580898a2f869 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -14,6 +14,7 @@ import type {
CreateWorkspaceFromIOUPaymentParams,
CreateWorkspaceParams,
DeleteMembersFromWorkspaceParams,
+ DeletePolicyDistanceRatesParams,
DeleteWorkspaceAvatarParams,
DeleteWorkspaceParams,
EnablePolicyCategoriesParams,
@@ -35,6 +36,7 @@ import type {
OpenWorkspaceParams,
OpenWorkspaceReimburseViewParams,
SetPolicyDistanceRatesDefaultCategoryParams,
+ SetPolicyDistanceRatesEnabledParams,
SetPolicyDistanceRatesUnitParams,
SetWorkspaceApprovalModeParams,
SetWorkspaceAutoReportingFrequencyParams,
@@ -42,6 +44,7 @@ import type {
SetWorkspaceAutoReportingParams,
SetWorkspacePayerParams,
SetWorkspaceReimbursementParams,
+ UpdatePolicyDistanceRateValueParams,
UpdateWorkspaceAvatarParams,
UpdateWorkspaceCustomUnitAndRateParams,
UpdateWorkspaceDescriptionParams,
@@ -263,22 +266,27 @@ function isCurrencySupportedForDirectReimbursement(currency: string) {
/**
* Check if the user has any active free policies (aka workspaces)
*/
-function hasActiveFreePolicy(policies: Array> | PoliciesRecord): boolean {
- const adminFreePolicies = Object.values(policies).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN);
+function hasActiveChatEnabledPolicies(policies: Array> | PoliciesRecord, includeOnlyFreePolicies = false): boolean {
+ const adminChatEnabledPolicies = Object.values(policies).filter(
+ (policy) =>
+ policy &&
+ ((policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) ||
+ (!includeOnlyFreePolicies && policy.type !== CONST.POLICY.TYPE.PERSONAL && policy.role === CONST.POLICY.ROLE.ADMIN && policy.isPolicyExpenseChatEnabled)),
+ );
- if (adminFreePolicies.length === 0) {
+ if (adminChatEnabledPolicies.length === 0) {
return false;
}
- if (adminFreePolicies.some((policy) => !policy?.pendingAction)) {
+ if (adminChatEnabledPolicies.some((policy) => !policy?.pendingAction)) {
return true;
}
- if (adminFreePolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
+ if (adminChatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
return true;
}
- if (adminFreePolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
+ if (adminChatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
return false;
}
@@ -306,7 +314,7 @@ function deleteWorkspace(policyID: string, policyName: string) {
errors: null,
},
},
- ...(!hasActiveFreePolicy(filteredPolicies)
+ ...(!hasActiveChatEnabledPolicies(filteredPolicies, true)
? [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -2875,7 +2883,7 @@ function createPolicyTag(policyID: string, tagName: string) {
tags: {
[tagName]: {
name: tagName,
- enabled: false,
+ enabled: true,
errors: null,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
@@ -3997,6 +4005,34 @@ function clearPolicyDistanceRatesErrorFields(policyID: string, customUnitID: str
});
}
+function clearDeleteDistanceRateError(policyID: string, customUnitID: string, rateID: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ customUnits: {
+ [customUnitID]: {
+ rates: {
+ [rateID]: {
+ errors: null,
+ },
+ },
+ },
+ },
+ });
+}
+
+function clearPolicyDistanceRateErrorFields(policyID: string, customUnitID: string, rateID: string, updatedErrorFields: ErrorFields) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ customUnits: {
+ [customUnitID]: {
+ rates: {
+ [rateID]: {
+ errorFields: updatedErrorFields,
+ },
+ },
+ },
+ },
+ });
+}
+
function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: CustomUnit) {
const optimisticData: OnyxUpdate[] = [
{
@@ -4020,7 +4056,7 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU
value: {
customUnits: {
[newCustomUnit.customUnitID]: {
- pendingFields: null,
+ pendingFields: {attributes: null},
},
},
},
@@ -4105,6 +4141,237 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn
API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData});
}
+/**
+ * Takes array of customUnitRates and removes pendingFields and errorFields from each rate - we don't want to send those via API
+ */
+function prepareCustomUnitRatesArray(customUnitRates: Rate[]): Rate[] {
+ const customUnitRateArray: Rate[] = [];
+ customUnitRates.forEach((rate) => {
+ const cleanedRate = {...rate};
+ delete cleanedRate.pendingFields;
+ delete cleanedRate.errorFields;
+ customUnitRateArray.push(cleanedRate);
+ });
+
+ return customUnitRateArray;
+}
+
+function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
+ const currentRates = customUnit.rates;
+ const optimisticRates: Record = {};
+ const successRates: Record = {};
+ const failureRates: Record = {};
+ const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
+
+ for (const rateID of Object.keys(customUnit.rates)) {
+ if (rateIDs.includes(rateID)) {
+ const foundRate = customUnitRates.find((rate) => rate.customUnitRateID === rateID);
+ optimisticRates[rateID] = {...foundRate, pendingFields: {rate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}};
+ successRates[rateID] = {...foundRate, pendingFields: {rate: null}};
+ failureRates[rateID] = {
+ ...currentRates[rateID],
+ pendingFields: {rate: null},
+ errorFields: {rate: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')},
+ };
+ }
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: optimisticRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: successRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: failureRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const params: UpdatePolicyDistanceRateValueParams = {
+ policyID,
+ customUnitID: customUnit.customUnitID,
+ customUnitRateArray: JSON.stringify(prepareCustomUnitRatesArray(customUnitRates)),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE, params, {optimisticData, successData, failureData});
+}
+
+function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
+ const currentRates = customUnit.rates;
+ const optimisticRates: Record = {};
+ const successRates: Record = {};
+ const failureRates: Record = {};
+ const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
+
+ for (const rateID of Object.keys(currentRates)) {
+ if (rateIDs.includes(rateID)) {
+ const foundRate = customUnitRates.find((rate) => rate.customUnitRateID === rateID);
+ optimisticRates[rateID] = {...foundRate, pendingFields: {enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}};
+ successRates[rateID] = {...foundRate, pendingFields: {enabled: null}};
+ failureRates[rateID] = {
+ ...currentRates[rateID],
+ pendingFields: {enabled: null},
+ errorFields: {enabled: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')},
+ };
+ }
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: optimisticRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: successRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: failureRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const params: SetPolicyDistanceRatesEnabledParams = {
+ policyID,
+ customUnitID: customUnit.customUnitID,
+ customUnitRateArray: JSON.stringify(prepareCustomUnitRatesArray(customUnitRates)),
+ };
+
+ API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED, params, {optimisticData, successData, failureData});
+}
+
+function deletePolicyDistanceRates(policyID: string, customUnit: CustomUnit, rateIDsToDelete: string[]) {
+ const currentRates = customUnit.rates;
+ const optimisticRates: Record = {};
+ const successRates: Record = {};
+ const failureRates: Record = {};
+
+ for (const rateID of Object.keys(currentRates)) {
+ if (rateIDsToDelete.includes(rateID)) {
+ optimisticRates[rateID] = {
+ ...currentRates[rateID],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ };
+ failureRates[rateID] = {
+ ...currentRates[rateID],
+ pendingAction: null,
+ errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'),
+ };
+ } else {
+ optimisticRates[rateID] = currentRates[rateID];
+ successRates[rateID] = currentRates[rateID];
+ }
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: optimisticRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: successRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: failureRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const params: DeletePolicyDistanceRatesParams = {
+ policyID,
+ customUnitID: customUnit.customUnitID,
+ customUnitRateID: rateIDsToDelete,
+ };
+
+ API.write(WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES, params, {optimisticData, successData, failureData});
+}
+
function setPolicyCustomTaxName(policyID: string, customTaxName: string) {
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
const originalCustomTaxName = policy?.taxRates?.name;
@@ -4266,7 +4533,7 @@ export {
updateWorkspaceMembersRole,
addMembersToWorkspace,
isAdminOfFreePolicy,
- hasActiveFreePolicy,
+ hasActiveChatEnabledPolicies,
setWorkspaceErrors,
clearCustomUnitErrors,
hideWorkspaceAlertMessage,
@@ -4335,6 +4602,7 @@ export {
generateCustomUnitID,
createPolicyDistanceRate,
clearCreateDistanceRateItemAndError,
+ clearDeleteDistanceRateError,
setPolicyDistanceRatesUnit,
setPolicyDistanceRatesDefaultCategory,
createPolicyTag,
@@ -4350,4 +4618,8 @@ export {
clearPolicyErrorField,
isCurrencySupportedForDirectReimbursement,
clearPolicyDistanceRatesErrorFields,
+ clearPolicyDistanceRateErrorFields,
+ updatePolicyDistanceRateValue,
+ setPolicyDistanceRatesEnabled,
+ deletePolicyDistanceRates,
};
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 3d9aa5646098..123614f2e0bb 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -4,7 +4,7 @@ import lodashHas from 'lodash/has';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
-import type {GetRouteForDraftParams, GetRouteParams} from '@libs/API/parameters';
+import type {GetRouteParams} from '@libs/API/parameters';
import {READ_COMMANDS} from '@libs/API/types';
import * as CollectionUtils from '@libs/CollectionUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -218,26 +218,13 @@ function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): Ony
* Gets the route for a set of waypoints
* Used so we can generate a map view of the provided waypoints
*/
-function getRoute(transactionID: string, waypoints: WaypointCollection) {
+function getRoute(transactionID: string, waypoints: WaypointCollection, isDraft: boolean) {
const parameters: GetRouteParams = {
transactionID,
waypoints: JSON.stringify(waypoints),
};
- API.read(READ_COMMANDS.GET_ROUTE, parameters, getOnyxDataForRouteRequest(transactionID));
-}
-
-/**
- * Gets the route for a set of waypoints
- * Used so we can generate a map view of the provided waypoints
- */
-function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) {
- const parameters: GetRouteForDraftParams = {
- transactionID,
- waypoints: JSON.stringify(waypoints),
- };
-
- API.read(READ_COMMANDS.GET_ROUTE_FOR_DRAFT, parameters, getOnyxDataForRouteRequest(transactionID, true));
+ API.read(isDraft ? READ_COMMANDS.GET_ROUTE_FOR_DRAFT : READ_COMMANDS.GET_ROUTE, parameters, getOnyxDataForRouteRequest(transactionID, isDraft));
}
/**
@@ -277,4 +264,4 @@ function clearError(transactionID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null});
}
-export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, getRouteForDraft, updateWaypoints, clearError};
+export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, updateWaypoints, clearError};
diff --git a/src/libs/markAllPolicyReportsAsRead.ts b/src/libs/markAllPolicyReportsAsRead.ts
new file mode 100644
index 000000000000..c3c719b1132e
--- /dev/null
+++ b/src/libs/markAllPolicyReportsAsRead.ts
@@ -0,0 +1,33 @@
+// eslint-disable-next-line you-dont-need-lodash-underscore/each
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report} from '@src/types/onyx';
+import * as ReportActionFile from './actions/Report';
+import * as ReportUtils from './ReportUtils';
+
+export default function markAllPolicyReportsAsRead(policyID: string) {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (allReports) => {
+ if (!allReports) {
+ return;
+ }
+
+ let delay = 0;
+ Object.keys(allReports).forEach((key: string) => {
+ const report: Report | null | undefined = allReports[key];
+ if (report?.policyID !== policyID || !ReportUtils.isUnread(report)) {
+ return;
+ }
+
+ setTimeout(() => {
+ ReportActionFile.readNewestAction(report?.reportID);
+ }, delay);
+
+ delay += 1000;
+ });
+ Onyx.disconnect(connectionID);
+ },
+ });
+}
diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx
index a314120fb0c6..a3d9c2fd99ff 100644
--- a/src/pages/EditReportFieldDropdownPage.tsx
+++ b/src/pages/EditReportFieldDropdownPage.tsx
@@ -37,42 +37,89 @@ type EditReportFieldDropdownPageOnyxProps = {
type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps;
+type ReportFieldDropdownData = {
+ text: string;
+ keyForList: string;
+ searchText: string;
+ tooltipText: string;
+};
+
+type ReportFieldDropdownSectionItem = {
+ data: ReportFieldDropdownData[];
+ shouldShow: boolean;
+ title?: string;
+};
+
function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) {
const [searchValue, setSearchValue] = useState('');
const styles = useThemeStyles();
const {getSafeAreaMargins} = useStyleUtils();
const {translate} = useLocalize();
const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]);
- const [headerMessage, setHeaderMessage] = useState('');
-
- const sections = useMemo(() => {
- const filteredRecentOptions = recentlyUsedOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase()));
- const filteredRestOfOptions = fieldOptions.filter((option) => !filteredRecentOptions.includes(option) && option.toLowerCase().includes(searchValue.toLowerCase()));
- setHeaderMessage(!filteredRecentOptions.length && !filteredRestOfOptions.length ? translate('common.noResultsFound') : '');
-
- return [
- {
- title: translate('common.recents'),
- shouldShow: filteredRecentOptions.length > 0,
- data: filteredRecentOptions.map((option) => ({
- text: option,
- keyForList: option,
- searchText: option,
- tooltipText: option,
- })),
- },
- {
- title: translate('common.all'),
- shouldShow: filteredRestOfOptions.length > 0,
- data: filteredRestOfOptions.map((option) => ({
+
+ const {sections, headerMessage} = useMemo(() => {
+ let newHeaderMessage = '';
+ const newSections: ReportFieldDropdownSectionItem[] = [];
+
+ if (searchValue) {
+ const filteredOptions = fieldOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase()));
+ newHeaderMessage = !filteredOptions.length ? translate('common.noResultsFound') : '';
+ newSections.push({
+ shouldShow: false,
+ data: filteredOptions.map((option) => ({
text: option,
keyForList: option,
searchText: option,
tooltipText: option,
})),
- },
- ];
- }, [fieldOptions, recentlyUsedOptions, searchValue, translate]);
+ });
+ } else {
+ const selectedValue = fieldValue;
+ if (selectedValue) {
+ newSections.push({
+ shouldShow: false,
+ data: [
+ {
+ text: selectedValue,
+ keyForList: selectedValue,
+ searchText: selectedValue,
+ tooltipText: selectedValue,
+ },
+ ],
+ });
+ }
+
+ const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((option) => option !== selectedValue);
+ if (filteredRecentlyUsedOptions.length > 0) {
+ newSections.push({
+ title: translate('common.recents'),
+ shouldShow: true,
+ data: filteredRecentlyUsedOptions.map((option) => ({
+ text: option,
+ keyForList: option,
+ searchText: option,
+ tooltipText: option,
+ })),
+ });
+ }
+
+ const filteredFieldOptions = fieldOptions.filter((option) => option !== selectedValue);
+ if (filteredFieldOptions.length > 0) {
+ newSections.push({
+ title: translate('common.all'),
+ shouldShow: true,
+ data: filteredFieldOptions.map((option) => ({
+ text: option,
+ keyForList: option,
+ searchText: option,
+ tooltipText: option,
+ })),
+ });
+ }
+ }
+
+ return {sections: newSections, headerMessage: newHeaderMessage};
+ }, [fieldValue, fieldOptions, recentlyUsedOptions, searchValue, translate]);
return (
{
- hasWaypointError.current = Boolean(lodashGet(transaction, 'errorFields.route') || lodashGet(transaction, 'errorFields.waypoints'));
-
- // When the loading goes from true to false, then we know the transaction has just been
- // saved to the server. Check for errors. If there are no errors, then the modal can be closed.
- if (prevIsLoading && !transaction.isLoading && !hasWaypointError.current) {
- Navigation.dismissModal(report.reportID);
- }
- }, [transaction, prevIsLoading, report]);
-
- /**
- * Save the changes to the original transaction object
- * @param {Object} waypoints
- */
- const saveTransaction = (waypoints) => {
- // If nothing was changed, simply go to transaction thread
- // We compare only addresses because numbers are rounded while backup
- const oldWaypoints = lodashGet(transactionBackup, 'comment.waypoints', {});
- const oldAddresses = _.mapObject(oldWaypoints, (waypoint) => _.pick(waypoint, 'address'));
- const addresses = _.mapObject(waypoints, (waypoint) => _.pick(waypoint, 'address'));
- if (_.isEqual(oldAddresses, addresses)) {
- Navigation.dismissModal(report.reportID);
- return;
- }
-
- IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints);
-
- // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them
- // until they come online again and sync with the server).
- if (isOffline) {
- Navigation.dismissModal(report.reportID);
- }
- };
-
- return (
-
- Navigation.goBack()}
- />
-
-
- );
-}
-
-EditRequestDistancePage.propTypes = propTypes;
-EditRequestDistancePage.defaultProps = defaultProps;
-EditRequestDistancePage.displayName = 'EditRequestDistancePage';
-export default withOnyx({
- transaction: {
- key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}`,
- },
- transactionBackup: {
- key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${props.transactionID}`,
- },
-})(EditRequestDistancePage);
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index de17d16a7c38..7d10e0e55e79 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -20,7 +20,6 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import EditRequestAmountPage from './EditRequestAmountPage';
-import EditRequestDistancePage from './EditRequestDistancePage';
import EditRequestReceiptPage from './EditRequestReceiptPage';
import EditRequestTagPage from './EditRequestTagPage';
import reportActionPropTypes from './home/report/reportActionPropTypes';
@@ -176,16 +175,6 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
);
}
- if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DISTANCE) {
- return (
-
- );
- }
-
return (
1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM);
@@ -89,7 +89,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction,
const isTaskReport = ReportUtils.isTaskReport(report);
const reportHeaderData = !isTaskReport && !isChatThread && report.parentReportID ? parentReport : report;
// Use sorted display names for the title for group chats on native small screen widths
- const title = ReportUtils.isGroupChat(report) ? getGroupChatName(report) : ReportUtils.getReportName(reportHeaderData);
+ const title = ReportUtils.isGroupChat(report) ? getGroupChatName(report, true) : ReportUtils.getReportName(reportHeaderData);
const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData);
const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData);
const isConcierge = ReportUtils.hasSingleParticipant(report) && participants.includes(CONST.ACCOUNT_ID.CONCIERGE);
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 242602b0654c..6adde9e69d20 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -399,7 +399,7 @@ function ReportScreen({
});
return () => {
interactionTask.cancel();
- if (!didSubscribeToReportLeavingEvents) {
+ if (!didSubscribeToReportLeavingEvents.current) {
return;
}
@@ -490,6 +490,10 @@ function ReportScreen({
if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) {
return;
}
+ // Ensures the optimistic report is created successfully
+ if (reportIDFromRoute !== report.reportID) {
+ return;
+ }
// Ensures subscription event succeeds when the report/workspace room is created optimistically.
// Check if the optimistic `OpenReport` or `AddWorkspaceRoom` has succeeded by confirming
// any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null.
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index c5ab9bbff1f5..0ac639d5367c 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -377,7 +377,7 @@ const ContextMenuActions: ContextMenuAction[] = [
const mentionWhisperMessage = ReportActionsUtils.getActionableMentionWhisperMessage(reportAction);
setClipboardMessage(mentionWhisperMessage);
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
- Clipboard.setString(Localize.translateLocal('iou.heldRequest', {comment: reportAction.message?.[1]?.text ?? ''}));
+ Clipboard.setString(Localize.translateLocal('iou.heldRequest'));
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
Clipboard.setString(Localize.translateLocal('iou.unheldRequest'));
} else if (content) {
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index 08a7836f928d..0e8b0cf97d1e 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -517,7 +517,9 @@ function ReportActionItem({
// This handles all historical actions from OldDot that we just want to display the message text
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
- children = ;
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLDCOMMENT) {
+ children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
children = ;
} else {
diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index c74bb40a18b6..68cd069608da 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -264,6 +264,10 @@ function ReportActionsView({
}, [isSmallScreenWidth, reportActions, isReportFullyVisible]);
useEffect(() => {
+ // Ensures the optimistic report is created successfully
+ if (route?.params?.reportID !== reportID) {
+ return;
+ }
// Ensures subscription event succeeds when the report/workspace room is created optimistically.
// Check if the optimistic `OpenReport` or `AddWorkspaceRoom` has succeeded by confirming
// any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null.
@@ -278,7 +282,7 @@ function ReportActionsView({
interactionTask.cancel();
};
}
- }, [report.pendingFields, didSubscribeToReportTypingEvents, reportID]);
+ }, [report.pendingFields, didSubscribeToReportTypingEvents, route, reportID]);
const onContentSizeChange = useCallback((w: number, h: number) => {
contentListHeight.current = h;
diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx
index 2c6ce7a2dabd..de145d5ef7e6 100644
--- a/src/pages/home/report/ReportAttachments.tsx
+++ b/src/pages/home/report/ReportAttachments.tsx
@@ -16,8 +16,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) {
const report = ReportUtils.getReport(reportID);
// In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource
- const decodedSource = decodeURI(route.params.source);
- const source = Number(decodedSource) || decodedSource;
+ const source = Number(route.params.source) || route.params.source;
const onCarouselAttachmentChange = useCallback(
(attachment: Attachment) => {
diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx
index 951888a443c1..7ff413f554b8 100644
--- a/src/pages/home/report/comment/TextCommentFragment.tsx
+++ b/src/pages/home/report/comment/TextCommentFragment.tsx
@@ -16,6 +16,7 @@ import CONST from '@src/CONST';
import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage';
import type {Message} from '@src/types/onyx/ReportAction';
import RenderCommentHTML from './RenderCommentHTML';
+import shouldRenderAsText from './shouldRenderAsText';
type TextCommentFragmentProps = {
/** The reportAction's source */
@@ -47,17 +48,17 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
- // If the only difference between fragment.text and fragment.html is tags
- // we render it as text, not as html.
- // This is done to render emojis with line breaks between them as text.
- const differByLineBreaksOnly = Str.replaceAll(html, ' ', '\n') === text;
-
- // Only render HTML if we have html in the fragment
- if (!differByLineBreaksOnly) {
+ // If the only difference between fragment.text and fragment.html is tags and emoji tag
+ // on native, we render it as text, not as html
+ // on other device, only render it as text if the only difference is tag
+ const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text);
+ if (!shouldRenderAsText(html, text) && !(containsOnlyEmojis && styleAsDeleted)) {
const editedTag = fragment.isEdited ? `` : '';
- const htmlContent = styleAsDeleted ? `${html}` : html;
+ const htmlWithDeletedTag = styleAsDeleted ? `${html}` : html;
+ const htmlContent = containsOnlyEmojis ? Str.replaceAll(htmlWithDeletedTag, '', '') : htmlWithDeletedTag;
let htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent;
+
if (styleAsMuted) {
htmlWithTag = `${htmlWithTag}`;
}
@@ -70,7 +71,6 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so
);
}
- const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text);
const message = isEmpty(iouMessage) ? text : iouMessage;
return (
diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.native.ts b/src/pages/home/report/comment/shouldRenderAsText/index.native.ts
new file mode 100644
index 000000000000..7c5758f8720d
--- /dev/null
+++ b/src/pages/home/report/comment/shouldRenderAsText/index.native.ts
@@ -0,0 +1,12 @@
+import Str from 'expensify-common/lib/str';
+
+/**
+ * Whether to render the report action as text
+ */
+export default function shouldRenderAsText(html: string, text: string): boolean {
+ // On native, we render emoji as text to prevent the large emoji is cut off when the action is edited.
+ // More info: https://github.com/Expensify/App/pull/35838#issuecomment-1964839350
+ const htmlWithoutLineBreak = Str.replaceAll(html, ' ', '\n');
+ const htmlWithoutEmojiOpenTag = Str.replaceAll(htmlWithoutLineBreak, '', '');
+ return Str.replaceAll(htmlWithoutEmojiOpenTag, '', '') === text;
+}
diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.ts b/src/pages/home/report/comment/shouldRenderAsText/index.ts
new file mode 100644
index 000000000000..f26f43c528eb
--- /dev/null
+++ b/src/pages/home/report/comment/shouldRenderAsText/index.ts
@@ -0,0 +1,8 @@
+import Str from 'expensify-common/lib/str';
+
+/**
+ * Whether to render the report action as text
+ */
+export default function shouldRenderAsText(html: string, text: string): boolean {
+ return Str.replaceAll(html, ' ', '\n') === text;
+}
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index abf932eff96d..f661cee00b56 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -216,7 +216,7 @@ function FloatingActionButtonAndPopover(props) {
text: translate('sidebarScreen.saveTheWorld'),
onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)),
},
- ...(!props.isLoading && !Policy.hasActiveFreePolicy(props.allPolicies)
+ ...(!props.isLoading && !Policy.hasActiveChatEnabledPolicies(props.allPolicies)
? [
{
displayInDefaultIconColor: true,
diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx
index 0d5c5b8a327b..2a5cba810759 100644
--- a/src/pages/iou/HoldReasonPage.tsx
+++ b/src/pages/iou/HoldReasonPage.tsx
@@ -8,6 +8,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -40,6 +41,7 @@ type HoldReasonPageProps = {
function HoldReasonPage({route}: HoldReasonPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
const {transactionID, reportID, backTo} = route.params;
@@ -110,7 +112,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) {
defaultValue={undefined}
label={translate('iou.reason')}
accessibilityLabel={translate('iou.reason')}
- autoFocus
+ ref={inputCallbackRef}
/>
diff --git a/src/pages/iou/MoneyRequestWaypointPage.tsx b/src/pages/iou/MoneyRequestWaypointPage.tsx
index c21aae7cf063..dd65b76c8d38 100644
--- a/src/pages/iou/MoneyRequestWaypointPage.tsx
+++ b/src/pages/iou/MoneyRequestWaypointPage.tsx
@@ -20,6 +20,7 @@ function MoneyRequestWaypointPage({transactionID = '', route}: MoneyRequestWaypo
// Put the transactionID into the route params so that WaypointEdit behaves the same when creating a new waypoint
// or editing an existing waypoint.
route={{
+ ...route,
params: {
...route.params,
transactionID,
diff --git a/src/pages/iou/NewDistanceRequestPage.js b/src/pages/iou/NewDistanceRequestPage.js
deleted file mode 100644
index 750ac5d0141e..000000000000
--- a/src/pages/iou/NewDistanceRequestPage.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useCallback, useEffect} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import DistanceRequest from '@components/DistanceRequest';
-import Navigation from '@libs/Navigation/Navigation';
-import reportPropTypes from '@pages/reportPropTypes';
-import * as IOU from '@userActions/IOU';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import {iouPropTypes} from './propTypes';
-
-const propTypes = {
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: iouPropTypes,
-
- /** The report on which the request is initiated on */
- report: reportPropTypes,
-
- /** Passed from the navigator */
- route: PropTypes.shape({
- /** Parameters the route gets */
- params: PropTypes.shape({
- /** Type of IOU */
- iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)),
- /** Id of the report on which the distance request is being created */
- reportID: PropTypes.string,
- }),
- }),
-};
-
-const defaultProps = {
- iou: {},
- report: {},
- route: {
- params: {
- iouType: '',
- reportID: '',
- },
- },
-};
-
-// This component is responsible for getting the transactionID from the IOU key, or creating the transaction if it doesn't exist yet, and then passing the transactionID.
-// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that DistanceRequest can subscribe to the transaction.
-function NewDistanceRequestPage({iou, report, route}) {
- const iouType = lodashGet(route, 'params.iouType', 'request');
- const isEditingNewRequest = Navigation.getActiveRoute().includes('address');
-
- useEffect(() => {
- if (iou.transactionID) {
- return;
- }
- IOU.setUpDistanceTransaction();
- }, [iou.transactionID]);
-
- const onSubmit = useCallback(() => {
- if (isEditingNewRequest) {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report.reportID));
- return;
- }
- IOU.navigateToNextPage(iou, iouType, report);
- }, [iou, iouType, isEditingNewRequest, report]);
-
- return (
-
- );
-}
-
-NewDistanceRequestPage.displayName = 'NewDistanceRequestPage';
-NewDistanceRequestPage.propTypes = propTypes;
-NewDistanceRequestPage.defaultProps = defaultProps;
-export default withOnyx({
- iou: {key: ONYXKEYS.IOU},
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID')}`,
- },
-})(NewDistanceRequestPage);
diff --git a/src/pages/iou/request/IOURequestRedirectToStartPage.js b/src/pages/iou/request/IOURequestRedirectToStartPage.js
index ee98c8006cdb..2da235743705 100644
--- a/src/pages/iou/request/IOURequestRedirectToStartPage.js
+++ b/src/pages/iou/request/IOURequestRedirectToStartPage.js
@@ -41,11 +41,11 @@ function IOURequestRedirectToStartPage({
// Redirect the person to the right start page using a rendom reportID
const optimisticReportID = ReportUtils.generateReportID();
if (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
} else if (iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.getRoute(iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
} else if (iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
}
// This useEffect should only run on mount which is why there are no dependencies being passed in the second parameter
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index 3dd6f08c0ce0..435121a76028 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -112,8 +112,8 @@ function IOURequestStepConfirmation({
const participants = useMemo(
() =>
_.map(transaction.participants, (participant) => {
- const participantReportID = lodashGet(participant, 'reportID', '');
- return participantReportID ? OptionsListUtils.getReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails);
+ const participantAccountID = lodashGet(participant, 'accountID', 0);
+ return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant);
}),
[transaction.participants, personalDetails],
);
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js
index 9e12381bc497..dad610cbc636 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.js
+++ b/src/pages/iou/request/step/IOURequestStepDistance.js
@@ -1,6 +1,7 @@
import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import Button from '@components/Button';
import DistanceRequestFooter from '@components/DistanceRequest/DistanceRequestFooter';
@@ -22,6 +23,7 @@ import * as IOU from '@userActions/IOU';
import * as MapboxToken from '@userActions/MapboxToken';
import * as Transaction from '@userActions/Transaction';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
import StepScreenWrapper from './StepScreenWrapper';
@@ -38,19 +40,24 @@ const propTypes = {
/** The transaction object being modified in Onyx */
transaction: transactionPropTypes,
+
+ /** backup version of the original transaction */
+ transactionBackup: transactionPropTypes,
};
const defaultProps = {
report: {},
transaction: {},
+ transactionBackup: {},
};
function IOURequestStepDistance({
report,
route: {
- params: {iouType, reportID, transactionID, backTo},
+ params: {action, iouType, reportID, transactionID, backTo},
},
transaction,
+ transactionBackup,
}) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();
@@ -76,6 +83,8 @@ function IOURequestStepDistance({
const nonEmptyWaypointsCount = useMemo(() => _.filter(_.keys(waypoints), (key) => !_.isEmpty(waypoints[key])).length, [waypoints]);
const duplicateWaypointsError = useMemo(() => nonEmptyWaypointsCount >= 2 && _.size(validatedWaypoints) !== nonEmptyWaypointsCount, [nonEmptyWaypointsCount, validatedWaypoints]);
const atLeastTwoDifferentWaypointsError = useMemo(() => _.size(validatedWaypoints) < 2, [validatedWaypoints]);
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const isCreatingNewRequest = Navigation.getActiveRoute().includes('start');
useEffect(() => {
MapboxToken.init();
@@ -86,8 +95,8 @@ function IOURequestStepDistance({
if (isOffline || !shouldFetchRoute) {
return;
}
- Transaction.getRouteForDraft(transactionID, validatedWaypoints);
- }, [shouldFetchRoute, transactionID, validatedWaypoints, isOffline]);
+ Transaction.getRoute(transactionID, validatedWaypoints, action === CONST.IOU.ACTION.CREATE);
+ }, [shouldFetchRoute, transactionID, validatedWaypoints, isOffline, action]);
useEffect(() => {
if (numberOfWaypoints <= numberOfPreviousWaypoints) {
@@ -112,9 +121,7 @@ function IOURequestStepDistance({
* @param {Number} index of the waypoint to edit
*/
const navigateToWaypointEditPage = (index) => {
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.REQUEST, transactionID, report.reportID, index, Navigation.getActiveRouteWithoutParams()),
- );
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(action, CONST.IOU.TYPE.REQUEST, transactionID, report.reportID, index, Navigation.getActiveRouteWithoutParams()));
};
const navigateToNextStep = useCallback(() => {
@@ -168,11 +175,14 @@ function IOURequestStepDistance({
setOptimisticWaypoints(newWaypoints);
// eslint-disable-next-line rulesdir/no-thenable-actions-in-views
- Promise.all([Transaction.removeWaypoint(transaction, emptyWaypointIndex.toString(), true), Transaction.updateWaypoints(transactionID, newWaypoints, true)]).then(() => {
- setOptimisticWaypoints(null);
+ Promise.all([
+ Transaction.removeWaypoint(transaction, emptyWaypointIndex.toString(), action === CONST.IOU.ACTION.CREATE),
+ Transaction.updateWaypoints(transactionID, newWaypoints, action === CONST.IOU.ACTION.CREATE),
+ ]).then(() => {
+ setOptimisticWaypoints(undefined);
});
},
- [transactionID, transaction, waypoints, waypointsList],
+ [transactionID, transaction, waypoints, waypointsList, action],
);
const submitWaypoints = useCallback(() => {
@@ -181,15 +191,42 @@ function IOURequestStepDistance({
setShouldShowAtLeastTwoDifferentWaypointsError(true);
return;
}
+ if (isEditing) {
+ // If nothing was changed, simply go to transaction thread
+ // We compare only addresses because numbers are rounded while backup
+ const oldWaypoints = lodashGet(transactionBackup, 'comment.waypoints', {});
+ const oldAddresses = _.mapObject(oldWaypoints, (waypoint) => _.pick(waypoint, 'address'));
+ const addresses = _.mapObject(waypoints, (waypoint) => _.pick(waypoint, 'address'));
+ if (_.isEqual(oldAddresses, addresses)) {
+ Navigation.dismissModal(report.reportID);
+ return;
+ }
+ IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints);
+ Navigation.dismissModal(report.reportID);
+ return;
+ }
+
navigateToNextStep();
- }, [atLeastTwoDifferentWaypointsError, duplicateWaypointsError, hasRouteError, isLoadingRoute, isLoading, navigateToNextStep]);
+ }, [
+ duplicateWaypointsError,
+ atLeastTwoDifferentWaypointsError,
+ hasRouteError,
+ isLoadingRoute,
+ isLoading,
+ isEditing,
+ navigateToNextStep,
+ transactionBackup,
+ waypoints,
+ transaction.transactionID,
+ report.reportID,
+ ]);
return (
<>
@@ -237,7 +274,7 @@ function IOURequestStepDistance({
large
style={[styles.w100, styles.mb4, styles.ph4, styles.flexShrink0]}
onPress={submitWaypoints}
- text={translate('common.next')}
+ text={translate(!isCreatingNewRequest ? 'common.save' : 'common.next')}
isLoading={!isOffline && (isLoadingRoute || shouldFetchRoute || isLoading)}
/>
@@ -250,4 +287,12 @@ IOURequestStepDistance.displayName = 'IOURequestStepDistance';
IOURequestStepDistance.propTypes = propTypes;
IOURequestStepDistance.defaultProps = defaultProps;
-export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepDistance);
+export default compose(
+ withWritableReportOrNotFound,
+ withFullTransactionOrNotFound,
+ withOnyx({
+ transactionBackup: {
+ key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${props.transactionID}`,
+ },
+ }),
+)(IOURequestStepDistance);
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
index 8375d9122340..4e61ac944aac 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
@@ -3,9 +3,8 @@ import React, {useMemo, useRef, useState} from 'react';
import type {TextInput} from 'react-native';
import {View} from 'react-native';
import type {Place} from 'react-native-google-places-autocomplete';
-import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
+import type {OnyxEntry} from 'react-native-onyx';
import AddressSearch from '@components/AddressSearch';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ConfirmModal from '@components/ConfirmModal';
@@ -27,12 +26,12 @@ import * as Transaction from '@userActions/Transaction';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Route as Routes} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Waypoint} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
type IOURequestStepWaypointOnyxProps = {
@@ -43,18 +42,9 @@ type IOURequestStepWaypointOnyxProps = {
};
type IOURequestStepWaypointProps = {
- route: {
- params: {
- iouType: ValueOf;
- transactionID: string;
- reportID: string;
- backTo: Routes | undefined;
- action: ValueOf;
- pageIndex: string;
- };
- };
transaction: OnyxEntry;
-} & IOURequestStepWaypointOnyxProps;
+} & IOURequestStepWaypointOnyxProps &
+ WithWritableReportOrNotFoundProps;
function IOURequestStepWaypoint({
route: {
@@ -117,7 +107,7 @@ function IOURequestStepWaypoint({
const waypointValue = values[`waypoint${pageIndex}`] ?? '';
// Allows letting you set a waypoint to an empty value
if (waypointValue === '') {
- Transaction.removeWaypoint(transaction, pageIndex, true);
+ Transaction.removeWaypoint(transaction, pageIndex, action === CONST.IOU.ACTION.CREATE);
}
// While the user is offline, the auto-complete address search will not work
@@ -133,13 +123,13 @@ function IOURequestStepWaypoint({
}
// Other flows will be handled by selecting a waypoint with selectWaypoint as this is mainly for the offline flow
- Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID));
};
const deleteStopAndHideModal = () => {
- Transaction.removeWaypoint(transaction, pageIndex, true);
+ Transaction.removeWaypoint(transaction, pageIndex, action === CONST.IOU.ACTION.CREATE);
setIsDeleteStopModalOpen(false);
- Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID));
};
const selectWaypoint = (values: Waypoint) => {
@@ -155,7 +145,7 @@ function IOURequestStepWaypoint({
Navigation.goBack(backTo);
return;
}
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
};
return (
@@ -170,7 +160,7 @@ function IOURequestStepWaypoint({
title={translate(waypointDescriptionKey)}
shouldShowBackButton
onBackButtonPress={() => {
- Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID));
}}
shouldShowThreeDotsButton={shouldShowThreeDotsButton}
shouldSetModalVisibility={false}
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.js b/src/pages/iou/request/step/withWritableReportOrNotFound.js
deleted file mode 100644
index 978b84f321d1..000000000000
--- a/src/pages/iou/request/step/withWritableReportOrNotFound.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import getComponentDisplayName from '@libs/getComponentDisplayName';
-import * as ReportUtils from '@libs/ReportUtils';
-import reportPropTypes from '@pages/reportPropTypes';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
-
-const propTypes = {
- /** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component.
- * That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */
- forwardedRef: PropTypes.func,
-
- /** The report corresponding to the reportID in the route params */
- report: reportPropTypes,
-
- route: IOURequestStepRoutePropTypes.isRequired,
-};
-
-const defaultProps = {
- forwardedRef: () => {},
- report: {},
-};
-
-export default function (WrappedComponent) {
- // eslint-disable-next-line rulesdir/no-negated-variables
- function WithWritableReportOrNotFound({forwardedRef, ...props}) {
- const {
- route: {
- params: {iouType},
- },
- report,
- } = props;
-
- const iouTypeParamIsInvalid = !_.contains(_.values(CONST.IOU.TYPE), iouType);
- const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
- if (iouTypeParamIsInvalid || !canUserPerformWriteAction) {
- return ;
- }
-
- return (
-
- );
- }
-
- WithWritableReportOrNotFound.propTypes = propTypes;
- WithWritableReportOrNotFound.defaultProps = defaultProps;
- WithWritableReportOrNotFound.displayName = `withWritableReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`;
-
- // eslint-disable-next-line rulesdir/no-negated-variables
- const WithWritableReportOrNotFoundWithRef = React.forwardRef((props, ref) => (
-
- ));
-
- WithWritableReportOrNotFoundWithRef.displayName = 'WithWritableReportOrNotFoundWithRef';
-
- return withOnyx({
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '0')}`,
- },
- })(WithWritableReportOrNotFoundWithRef);
-}
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
new file mode 100644
index 000000000000..d5d27d8268b1
--- /dev/null
+++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
@@ -0,0 +1,55 @@
+import type {RouteProp} from '@react-navigation/core';
+import type {ComponentType, ForwardedRef, RefAttributes} from 'react';
+import React, {forwardRef} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import getComponentDisplayName from '@libs/getComponentDisplayName';
+import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types';
+import * as ReportUtils from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Report} from '@src/types/onyx';
+
+type WithWritableReportOrNotFoundOnyxProps = {
+ /** The report corresponding to the reportID in the route params */
+ report: OnyxEntry;
+};
+
+type Route = RouteProp;
+
+type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & {route: Route};
+
+export default function (
+ WrappedComponent: ComponentType>,
+): React.ComponentType, keyof WithWritableReportOrNotFoundOnyxProps>> {
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ function WithWritableReportOrNotFound(props: TProps, ref: ForwardedRef) {
+ const {report = {reportID: ''}, route} = props;
+ const iouTypeParamIsInvalid = !Object.values(CONST.IOU.TYPE).includes(route.params?.iouType);
+ const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
+
+ if (iouTypeParamIsInvalid || !canUserPerformWriteAction) {
+ return ;
+ }
+
+ return (
+
+ );
+ }
+
+ WithWritableReportOrNotFound.displayName = `withWritableReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`;
+
+ return withOnyx, WithWritableReportOrNotFoundOnyxProps>({
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID ?? '0'}`,
+ },
+ })(forwardRef(WithWritableReportOrNotFound));
+}
+
+export type {WithWritableReportOrNotFoundProps};
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
index 724c665b131a..badbdeb86d14 100644
--- a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
+++ b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
@@ -8,6 +8,7 @@ import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
@@ -43,6 +44,7 @@ function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyRe
const {keyboardHeight} = useKeyboardState();
const {windowHeight} = useWindowDimensions();
const {top: safeAreaInsetsTop} = useSafeAreaInsets();
+ const {inputCallbackRef} = useAutoFocusInput();
const {reason, backTo} = route.params;
const {isOffline} = useNetwork({
@@ -132,6 +134,7 @@ function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyRe
return;
}
updateMultilineInputRange(el);
+ inputCallbackRef(el);
}}
containerStyles={[baseResponseInputContainerStyle, StyleUtils.getMaximumHeight(responseInputMaxHeight)]}
shouldSaveDraft
diff --git a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx
index d6f9ea29ac83..0a361f3f8e85 100644
--- a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx
+++ b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx
@@ -15,6 +15,7 @@ import validateRateValue from '@libs/PolicyDistanceRatesUtils';
import Navigation from '@navigation/Navigation';
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 {createPolicyDistanceRate, generateCustomUnitID} from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -33,6 +34,7 @@ type CreateDistanceRatePageProps = CreateDistanceRatePageOnyxProps & StackScreen
function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) {
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
+ const policyID = route.params.policyID;
const currency = policy?.outputCurrency ?? CONST.CURRENCY.USD;
const customUnits = policy?.customUnits ?? {};
const customUnitID = customUnits[Object.keys(customUnits)[0]]?.customUnitID ?? '';
@@ -53,40 +55,45 @@ function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) {
enabled: true,
};
- createPolicyDistanceRate(route.params.policyID, customUnitID, newRate);
+ createPolicyDistanceRate(policyID, customUnitID, newRate);
Navigation.goBack();
};
return (
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx
new file mode 100644
index 000000000000..965096ffa529
--- /dev/null
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx
@@ -0,0 +1,176 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+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 * as CurrencyUtils from '@libs/CurrencyUtils';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+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';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {Rate} from '@src/types/onyx/Policy';
+
+type PolicyDistanceRateDetailsPageOnyxProps = {
+ /** Policy details */
+ policy: OnyxEntry;
+};
+
+type PolicyDistanceRateDetailsPageProps = PolicyDistanceRateDetailsPageOnyxProps & StackScreenProps;
+
+function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetailsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {windowWidth} = useWindowDimensions();
+ const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
+ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
+
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const customUnits = policy?.customUnits ?? {};
+ const customUnit = customUnits[Object.keys(customUnits)[0]];
+ const rate = customUnit.rates[rateID];
+ const currency = rate.currency ?? CONST.CURRENCY.USD;
+ const canDeleteRate = Object.values(customUnit.rates).filter((distanceRate) => distanceRate.enabled).length > 1 || !rate.enabled;
+ const canDisableRate = Object.values(customUnit.rates).filter((distanceRate) => distanceRate.enabled).length > 1;
+ const errorFields = rate.errorFields;
+
+ const editRateValue = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_EDIT.getRoute(policyID, rateID));
+ };
+
+ const toggleRate = () => {
+ if (!rate.enabled || canDisableRate) {
+ Policy.setPolicyDistanceRatesEnabled(policyID, customUnit, [{...rate, enabled: !rate.enabled}]);
+ } else {
+ setIsWarningModalVisible(true);
+ }
+ };
+
+ const deleteRate = () => {
+ Navigation.goBack();
+ Policy.deletePolicyDistanceRates(policyID, customUnit, [rateID]);
+ setIsDeleteModalVisible(false);
+ };
+
+ const rateValueToDisplay = CurrencyUtils.convertAmountToDisplayString(rate.rate, currency);
+ const unitToDisplay = translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`);
+
+ const threeDotsMenuItems = [
+ {
+ icon: Expensicons.Trashcan,
+ text: translate('workspace.distanceRates.deleteDistanceRate'),
+ onSelected: () => {
+ if (canDeleteRate) {
+ setIsDeleteModalVisible(true);
+ return;
+ }
+ setIsWarningModalVisible(true);
+ },
+ },
+ ];
+
+ const clearErrorFields = (fieldName: keyof Rate) => {
+ Policy.clearPolicyDistanceRateErrorFields(policyID, customUnit.customUnitID, rateID, {...errorFields, [fieldName]: null});
+ };
+
+ return (
+
+
+
+
+
+
+ clearErrorFields('enabled')}
+ >
+
+ {translate('workspace.distanceRates.enableRate')}
+
+
+
+ clearErrorFields('rate')}
+ >
+
+
+ setIsWarningModalVisible(false)}
+ isVisible={isWarningModalVisible}
+ title={translate('workspace.distanceRates.oopsNotSoFast')}
+ prompt={translate('workspace.distanceRates.workspaceNeeds')}
+ confirmText={translate('common.buttonConfirm')}
+ shouldShowCancelButton={false}
+ />
+ setIsDeleteModalVisible(false)}
+ prompt={translate('workspace.distanceRates.areYouSureDelete', {count: 1})}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
+
+
+
+
+
+ );
+}
+
+PolicyDistanceRateDetailsPage.displayName = 'PolicyDistanceRateDetailsPage';
+
+export default withOnyx({
+ policy: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`,
+ },
+})(PolicyDistanceRateDetailsPage);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx
new file mode 100644
index 000000000000..be85ee680d36
--- /dev/null
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx
@@ -0,0 +1,112 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {Keyboard} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import AmountForm from '@components/AmountForm';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapperWithRef from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import validateRateValue from '@libs/PolicyDistanceRatesUtils';
+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';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/PolicyDistanceRateEditForm';
+import type * as OnyxTypes from '@src/types/onyx';
+
+type PolicyDistanceRateEditPageOnyxProps = {
+ /** Policy details */
+ policy: OnyxEntry;
+};
+
+type PolicyDistanceRateEditPageProps = PolicyDistanceRateEditPageOnyxProps & StackScreenProps;
+
+function PolicyDistanceRateEditPage({policy, route}: PolicyDistanceRateEditPageProps) {
+ const styles = useThemeStyles();
+ const {translate, toLocaleDigit} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const customUnits = policy?.customUnits ?? {};
+ const customUnit = customUnits[Object.keys(customUnits)[0]];
+ const rate = customUnit.rates[rateID];
+ const currency = rate.currency ?? CONST.CURRENCY.USD;
+ const currentRateValue = (rate.rate ?? 0).toString();
+
+ const submitRate = (values: FormOnyxValues) => {
+ Policy.updatePolicyDistanceRateValue(policyID, customUnit, [{...rate, rate: Number(values.rate) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET}]);
+ Keyboard.dismiss();
+ Navigation.goBack();
+ };
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => validateRateValue(values, currency, toLocaleDigit),
+ [currency, toLocaleDigit],
+ );
+
+ return (
+
+
+
+
+ Navigation.goBack()}
+ />
+
+
+
+
+
+
+
+ );
+}
+
+PolicyDistanceRateEditPage.displayName = 'PolicyDistanceRateEditPage';
+
+export default withOnyx({
+ policy: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`,
+ },
+})(PolicyDistanceRateEditPage);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index 32531da0dd25..93accdb10b28 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -51,6 +51,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
const {translate} = useLocalize();
const [selectedDistanceRates, setSelectedDistanceRates] = useState([]);
const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
+ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const dropdownButtonRef = useRef(null);
const policyID = route.params.policyID;
@@ -59,6 +60,10 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
[policy?.customUnits],
);
const customUnitRates: Record = useMemo(() => customUnit?.rates ?? {}, [customUnit]);
+ const canDeleteSelectedRates = selectedDistanceRates.length !== Object.values(customUnitRates).length;
+ const canDisableSelectedRates = Object.values(customUnitRates)
+ .filter((rate: Rate) => !selectedDistanceRates.includes(rate))
+ .some((rate) => rate.enabled);
function fetchDistanceRates() {
Policy.openPolicyDistanceRatesPage(policyID);
@@ -66,9 +71,14 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
const dismissError = useCallback(
(item: RateForList) => {
+ if (customUnitRates[item.value].errors) {
+ Policy.clearDeleteDistanceRateError(policyID, customUnit?.customUnitID ?? '', item.value);
+ return;
+ }
+
Policy.clearCreateDistanceRateItemAndError(policyID, customUnit?.customUnitID ?? '', item.value);
},
- [customUnit?.customUnitID, policyID],
+ [customUnit?.customUnitID, customUnitRates, policyID],
);
const {isOffline} = useNetwork({onReconnect: fetchDistanceRates});
@@ -87,7 +97,8 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
)}`,
keyForList: value.customUnitRateID ?? '',
isSelected: selectedDistanceRates.find((rate) => rate.customUnitRateID === value.customUnitRateID) !== undefined,
- pendingAction: value.pendingAction,
+ isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ pendingAction: value.pendingAction ?? value.pendingFields?.rate ?? value.pendingFields?.enabled,
errors: value.errors ?? undefined,
rightElement: (
@@ -114,30 +125,49 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(policyID));
};
- const editRate = () => {
- // Navigation.navigate(ROUTES.WORKSPACE_EDIT_DISTANCE_RATE.getRoute(policyID, rateID));
+ const openRateDetails = (rate: RateForList) => {
+ setSelectedDistanceRates([]);
+ Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_DETAILS.getRoute(policyID, rate.value));
};
const disableRates = () => {
- if (selectedDistanceRates.length !== Object.values(customUnitRates).length) {
- // run enableWorkspaceDistanceRates for all selected rows
+ if (customUnit === undefined) {
return;
}
- setIsWarningModalVisible(true);
+ Policy.setPolicyDistanceRatesEnabled(
+ policyID,
+ customUnit,
+ selectedDistanceRates.filter((rate) => rate.enabled).map((rate) => ({...rate, enabled: false})),
+ );
+ setSelectedDistanceRates([]);
};
const enableRates = () => {
- // run enableWorkspaceDistanceRates for all selected rows
+ if (customUnit === undefined) {
+ return;
+ }
+
+ Policy.setPolicyDistanceRatesEnabled(
+ policyID,
+ customUnit,
+ selectedDistanceRates.filter((rate) => !rate.enabled).map((rate) => ({...rate, enabled: true})),
+ );
+ setSelectedDistanceRates([]);
};
const deleteRates = () => {
- if (selectedDistanceRates.length !== Object.values(customUnitRates).length) {
- // run deleteWorkspaceDistanceRates for all selected rows
+ if (customUnit === undefined) {
return;
}
- setIsWarningModalVisible(true);
+ Policy.deletePolicyDistanceRates(
+ policyID,
+ customUnit,
+ selectedDistanceRates.map((rate) => rate.customUnitRateID ?? ''),
+ );
+ setSelectedDistanceRates([]);
+ setIsDeleteModalVisible(false);
};
const toggleRate = (rate: RateForList) => {
@@ -152,7 +182,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
if (selectedDistanceRates.length === Object.values(customUnitRates).length) {
setSelectedDistanceRates([]);
} else {
- setSelectedDistanceRates([...Object.values(customUnitRates)]);
+ setSelectedDistanceRates([...Object.values(customUnitRates).filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)]);
}
};
@@ -166,27 +196,27 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
const getBulkActionsButtonOptions = () => {
const options: Array> = [
{
- text: translate(`workspace.distanceRates.${selectedDistanceRates.length <= 1 ? 'deleteRate' : 'deleteRates'}`),
+ text: translate('workspace.distanceRates.deleteRates', {count: selectedDistanceRates.length}),
value: CONST.POLICY.DISTANCE_RATES_BULK_ACTION_TYPES.DELETE,
icon: Expensicons.Trashcan,
- onSelected: deleteRates,
+ onSelected: () => (canDeleteSelectedRates ? setIsDeleteModalVisible(true) : setIsWarningModalVisible(true)),
},
];
const enabledRates = selectedDistanceRates.filter((rate) => rate.enabled);
if (enabledRates.length > 0) {
options.push({
- text: translate(`workspace.distanceRates.${enabledRates.length <= 1 ? 'disableRate' : 'disableRates'}`),
+ text: translate('workspace.distanceRates.disableRates', {count: enabledRates.length}),
value: CONST.POLICY.DISTANCE_RATES_BULK_ACTION_TYPES.DISABLE,
icon: Expensicons.DocumentSlash,
- onSelected: disableRates,
+ onSelected: () => (canDisableSelectedRates ? disableRates() : setIsWarningModalVisible(true)),
});
}
const disabledRates = selectedDistanceRates.filter((rate) => !rate.enabled);
if (disabledRates.length > 0) {
options.push({
- text: translate(`workspace.distanceRates.${disabledRates.length <= 1 ? 'enableRate' : 'enableRates'}`),
+ text: translate('workspace.distanceRates.enableRates', {count: disabledRates.length}),
value: CONST.POLICY.DISTANCE_RATES_BULK_ACTION_TYPES.ENABLE,
icon: Expensicons.DocumentSlash,
onSelected: enableRates,
@@ -271,7 +301,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
canSelectMultiple
sections={[{data: distanceRatesList, indexOffset: 0, isDisabled: false}]}
onCheckboxPress={toggleRate}
- onSelectRow={editRate}
+ onSelectRow={openRateDetails}
onSelectAll={toggleAllRates}
onDismissError={dismissError}
showScrollIndicator
@@ -288,6 +318,16 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
confirmText={translate('common.buttonConfirm')}
shouldShowCancelButton={false}
/>
+ setIsDeleteModalVisible(false)}
+ prompt={translate('workspace.distanceRates.areYouSureDelete', {count: selectedDistanceRates.length})}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
index 83b096db1301..dbfb853b38a0 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
@@ -1,5 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -9,6 +10,7 @@ import type {ListItem} from '@components/SelectionList/types';
import type {UnitItemType} from '@components/UnitPicker';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
@@ -47,7 +49,14 @@ function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSet
};
const setNewCategory = (category: ListItem) => {
- Policy.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, {...customUnit, defaultCategory: category.text});
+ if (!category.searchText) {
+ return;
+ }
+
+ Policy.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, {
+ ...customUnit,
+ defaultCategory: defaultCategory === category.searchText ? '' : category.searchText,
+ });
};
const clearErrorFields = (fieldName: keyof CustomUnit) => {
@@ -55,10 +64,10 @@ function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSet
};
return (
-
-
+
+
- clearErrorFields('attributes')}
- >
-
-
- {policy?.areCategoriesEnabled && (
+ clearErrorFields('defaultCategory')}
+ onClose={() => clearErrorFields('attributes')}
>
-
- )}
+ {policy?.areCategoriesEnabled && (
+ clearErrorFields('defaultCategory')}
+ >
+
+
+ )}
+
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx
index 0d1a8f1629c7..8fbfa7b79292 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx
@@ -39,13 +39,13 @@ function WorkspaceTaxesSettingsPage({
pendingAction: policy?.taxRates?.pendingFields?.name,
},
{
- title: policy?.taxRates?.taxes[policy?.taxRates?.defaultExternalID]?.name,
+ title: policy?.taxRates?.taxes?.[policy?.taxRates?.defaultExternalID]?.name,
description: translate('workspace.taxes.workspaceDefault'),
action: () => Navigation.navigate(ROUTES.WORKSPACE_TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT.getRoute(policyID)),
pendingAction: policy?.taxRates?.pendingFields?.defaultExternalID,
},
{
- title: policy?.taxRates?.taxes[policy?.taxRates?.foreignTaxDefault]?.name,
+ title: policy?.taxRates?.taxes?.[policy?.taxRates?.foreignTaxDefault]?.name,
description: translate('workspace.taxes.foreignDefault'),
action: () => Navigation.navigate(ROUTES.WORKSPACE_TAXES_SETTINGS_FOREIGN_CURRENCY_DEFAULT.getRoute(policyID)),
pendingAction: policy?.taxRates?.pendingFields?.foreignTaxDefault,
diff --git a/src/setup/addUtilsToWindow.ts b/src/setup/addUtilsToWindow.ts
index 9991a3dc07cd..d2d11e138431 100644
--- a/src/setup/addUtilsToWindow.ts
+++ b/src/setup/addUtilsToWindow.ts
@@ -1,5 +1,6 @@
import Onyx from 'react-native-onyx';
import * as Environment from '@libs/Environment/Environment';
+import markAllPolicyReportsAsRead from '@libs/markAllPolicyReportsAsRead';
import * as Session from '@userActions/Session';
/**
@@ -44,5 +45,9 @@ export default function addUtilsToWindow() {
};
window.setSupportToken = Session.setSupportAuthToken;
+
+ // Workaround to give employees the ability to mark reports as read via the JS console
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any).markAllPolicyReportsAsRead = markAllPolicyReportsAsRead;
});
}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 8a65cabaf19f..120b848bd5a4 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -286,6 +286,10 @@ const styles = (theme: ThemeColors) =>
...wordBreak.breakWord,
...spacing.pr4,
},
+ emojiTooltipWrapper: {
+ ...spacing.p2,
+ borderRadius: 8,
+ },
mentionSuggestionsAvatarContainer: {
width: 24,
@@ -4099,9 +4103,12 @@ const styles = (theme: ThemeColors) =>
gap: 16,
},
+ reportActionItemImagesContainer: {
+ margin: 4,
+ },
+
reportActionItemImages: {
flexDirection: 'row',
- margin: 4,
borderRadius: 12,
overflow: 'hidden',
height: variables.reportActionImagesSingleImageHeight,
diff --git a/src/types/form/PolicyDistanceRateEditForm.ts b/src/types/form/PolicyDistanceRateEditForm.ts
new file mode 100644
index 000000000000..2c7cb97b08d8
--- /dev/null
+++ b/src/types/form/PolicyDistanceRateEditForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ RATE: 'rate',
+} as const;
+
+type InputID = ValueOf;
+
+type PolicyDistanceRateEditForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.RATE]: string;
+ }
+>;
+
+export type {PolicyDistanceRateEditForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index f8bc009b7172..1f305769cec7 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -46,4 +46,5 @@ export type {WorkspaceTaxNameForm} from './WorkspaceTaxNameForm';
export type {WorkspaceTaxValueForm} from './WorkspaceTaxValueForm';
export type {WorkspaceTaxCustomName} from './WorkspaceTaxCustomName';
export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm';
+export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm';
export type {default as Form} from './Form';
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index e3ba941482a0..196267dc28cc 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -37,6 +37,11 @@ type OriginalMessageHold = {
originalMessage: unknown;
};
+type OriginalMessageHoldComment = {
+ actionName: typeof CONST.REPORT.ACTIONS.TYPE.HOLDCOMMENT;
+ originalMessage: unknown;
+};
+
type OriginalMessageUnHold = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.UNHOLD;
originalMessage: unknown;
@@ -301,6 +306,7 @@ type OriginalMessage =
| OriginalMessageClosed
| OriginalMessageCreated
| OriginalMessageHold
+ | OriginalMessageHoldComment
| OriginalMessageUnHold
| OriginalMessageRenamed
| OriginalMessageChronosOOOList
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index cc2688e7a137..5165fa2ee128 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -9,8 +9,9 @@ type Rate = OnyxCommon.OnyxValueWithOfflineFeedback<{
rate?: number;
currency?: string;
customUnitRateID?: string;
- errors?: OnyxCommon.Errors;
enabled?: boolean;
+ errors?: OnyxCommon.Errors;
+ errorFields?: OnyxCommon.ErrorFields;
}>;
type Attributes = {
diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js
index 9fbea1df862e..adfa35a57ad8 100644
--- a/tests/unit/ReportUtilsTest.js
+++ b/tests/unit/ReportUtilsTest.js
@@ -552,6 +552,7 @@ describe('ReportUtils', () => {
const paidPolicy = {
id: 'ef72dfeb',
type: CONST.POLICY.TYPE.TEAM,
+ autoReporting: true,
autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT,
};
Promise.all([