From 61f49617e333c385131d42e436c22428a8cda27c Mon Sep 17 00:00:00 2001
From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com>
Date: Thu, 27 Feb 2025 18:21:46 +0100
Subject: [PATCH 01/30] Implement ChangeReportPolicy
---
.../emptystate__receiptfairy.svg | 155 ++++++++++++++
src/CONST.ts | 1 +
src/ROUTES.ts | 8 +
src/SCREENS.ts | 6 +
.../ChangePolicyEducationalMenu.tsx | 53 +++++
.../ChangeWorkspaceMenuSectionList.tsx | 63 ++++++
src/components/Icon/Illustrations.ts | 2 +
.../ProcessMoneyRequestHoldMenu.tsx | 2 +-
src/languages/en.ts | 7 +
src/languages/es.ts | 7 +
.../parameters/ChangeReportPolicyParams.ts | 7 +
src/libs/API/parameters/index.ts | 1 +
src/libs/API/types.ts | 4 +
.../ModalStackNavigators/index.tsx | 11 +
.../FeatureTrainingModalNavigator.tsx | 5 +
.../Navigators/RightModalNavigator.tsx | 4 +
src/libs/Navigation/linkingConfig/config.ts | 6 +
src/libs/Navigation/types.ts | 10 +
src/libs/ReportActionsUtils.ts | 5 -
src/libs/ReportUtils.ts | 49 +++++
src/libs/actions/Report.ts | 190 ++++++++++++++++++
src/pages/ChangePolicyEducationalModal.tsx | 36 ++++
src/pages/ReportChangeWorkspacePage.tsx | 188 +++++++++++++++++
.../home/report/withReportOrNotFound.tsx | 4 +-
src/styles/variables.ts | 1 +
src/types/onyx/DismissedProductTraining.ts | 7 +
src/types/onyx/OriginalMessage.ts | 12 +-
27 files changed, 836 insertions(+), 8 deletions(-)
create mode 100644 assets/images/product-illustrations/emptystate__receiptfairy.svg
create mode 100644 src/components/ChangePolicyEducationalMenu.tsx
create mode 100644 src/components/ChangeWorkspaceMenuSectionList.tsx
create mode 100644 src/libs/API/parameters/ChangeReportPolicyParams.ts
create mode 100644 src/pages/ChangePolicyEducationalModal.tsx
create mode 100644 src/pages/ReportChangeWorkspacePage.tsx
diff --git a/assets/images/product-illustrations/emptystate__receiptfairy.svg b/assets/images/product-illustrations/emptystate__receiptfairy.svg
new file mode 100644
index 0000000000000..2c25f7dd11f1e
--- /dev/null
+++ b/assets/images/product-illustrations/emptystate__receiptfairy.svg
@@ -0,0 +1,155 @@
+
diff --git a/src/CONST.ts b/src/CONST.ts
index 29223e598d8cc..3791f20f87f75 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -6686,6 +6686,7 @@ const CONST = {
GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip',
SCAN_TEST_TOOLTIP: 'scanTestTooltip',
},
+ CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal',
SMART_BANNER_HEIGHT: 152,
NAVIGATION_TESTS: {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 86504d344c969..00cd0bd16921e 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -396,6 +396,10 @@ const ROUTES = {
route: 'r/:reportID/details/export/:connectionName',
getRoute: (reportID: string, connectionName: ConnectionName, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/export/${connectionName as string}` as const, backTo),
},
+ REPORT_WITH_ID_CHANGE_WORKSPACE: {
+ route: 'r/:reportID/change-workspace',
+ getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-workspace` as const, backTo),
+ },
REPORT_SETTINGS: {
route: 'r/:reportID/settings',
getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings` as const, backTo),
@@ -1615,6 +1619,10 @@ const ROUTES = {
route: 'hold-expense-educational',
getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo),
},
+ CHANGE_POLICY_EDUCATIONAL: {
+ route: 'change-workspace-educational',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('change-workspace-educational', backTo),
+ },
TRAVEL_MY_TRIPS: 'travel',
TRAVEL_TCS: {
route: 'travel/terms/:domain/accept',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 5a8b0c75d5c01..bed7cc4b3cbd1 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -164,6 +164,7 @@ const SCREENS = {
DETAILS: 'Details',
PROFILE: 'Profile',
REPORT_DETAILS: 'Report_Details',
+ REPORT_CHANGE_WORKSPACE: 'ReportChangeWorkspace',
WORKSPACE_CONFIRMATION: 'Workspace_Confirmation',
REPORT_SETTINGS: 'Report_Settings',
REPORT_DESCRIPTION: 'Report_Description',
@@ -332,6 +333,10 @@ const SCREENS = {
EXPORT: 'Report_Details_Export',
},
+ REPORT_CHANGE_WORKSPACE: {
+ ROOT: 'ReportChangeWorkspace_Root',
+ },
+
WORKSPACE_CONFIRMATION: {ROOT: 'Workspace_Confirmation_Root'},
WORKSPACE: {
@@ -645,6 +650,7 @@ const SCREENS = {
DETAILS_ROOT: 'Details_Root',
PROFILE_ROOT: 'Profile_Root',
PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root',
+ CHANGE_POLICY_EDUCATIONAL_ROOT: 'ChangePolicyEducational_Root',
REPORT_DESCRIPTION_ROOT: 'Report_Description_Root',
REPORT_PARTICIPANTS: {
ROOT: 'ReportParticipants_Root',
diff --git a/src/components/ChangePolicyEducationalMenu.tsx b/src/components/ChangePolicyEducationalMenu.tsx
new file mode 100644
index 0000000000000..20ecbeeffafc7
--- /dev/null
+++ b/src/components/ChangePolicyEducationalMenu.tsx
@@ -0,0 +1,53 @@
+import {useNavigation} from '@react-navigation/native';
+import React, {useEffect} from 'react';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import FeatureTrainingModal from './FeatureTrainingModal';
+import HoldMenuSectionList from './HoldMenuSectionList';
+import * as Illustrations from './Icon/Illustrations';
+
+
+type ChangePolicyEducationalMenuProps = {
+ /** Method to trigger when pressing outside of the popover menu to close it */
+ onClose: () => void;
+
+ /** Method to trigger when pressing confirm button */
+ onConfirm: () => void;
+};
+
+function ChangePolicyEducationalMenu({onClose, onConfirm}: ChangePolicyEducationalMenuProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ const unsub = navigation.addListener('beforeRemove', () => {
+ onClose();
+ });
+ return unsub;
+ }, [navigation, onClose]);
+
+ return (
+
+
+
+ );
+}
+
+ChangePolicyEducationalMenu.displayName = 'ChangePolicyEducationalMenu';
+
+export default ChangePolicyEducationalMenu;
diff --git a/src/components/ChangeWorkspaceMenuSectionList.tsx b/src/components/ChangeWorkspaceMenuSectionList.tsx
new file mode 100644
index 0000000000000..846bd61dacb44
--- /dev/null
+++ b/src/components/ChangeWorkspaceMenuSectionList.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {ImageSourcePropType} from 'react-native';
+import type {SvgProps} from 'react-native-svg';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import type {TranslationPaths} from '@src/languages/types';
+import Icon from './Icon';
+import * as Illustrations from './Icon/Illustrations';
+import Text from './Text';
+
+type ChangeWorkspaceMenuSection = {
+ /** The icon supplied with the section */
+ icon: React.FC | ImageSourcePropType;
+
+ /** Translation key for the title */
+ titleTranslationKey: TranslationPaths;
+};
+
+function ChangeWorkspaceMenuSectionList() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const holdMenuSections: ChangeWorkspaceMenuSection[] = [
+ {
+ icon: Illustrations.FolderOpen,
+ titleTranslationKey: 'iou.changePolicyEducational.reCategorize',
+ },
+ {
+ icon: Illustrations.Workflows,
+ titleTranslationKey: 'iou.changePolicyEducational.workflows',
+ },
+ ];
+
+ return (
+ <>
+ {holdMenuSections.map((section, i) => (
+
+
+
+ {translate(section.titleTranslationKey)}
+
+
+ ))}
+ >
+ );
+}
+
+ChangeWorkspaceMenuSectionList.displayName = 'ChangeWorkspaceMenuSectionList';
+
+export type {ChangeWorkspaceMenuSection};
+
+export default ChangeWorkspaceMenuSectionList;
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 247958b5b8b2a..aac5875be4863 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -37,6 +37,7 @@ import ConciergeExclamation from '@assets/images/product-illustrations/concierge
import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg';
import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg';
import HoldExpense from '@assets/images/product-illustrations/emptystate__holdexpense.svg';
+import ReceiptFairy from '@assets/images/product-illustrations/emptystate__receiptfairy.svg';
import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg';
import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg';
import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg';
@@ -229,6 +230,7 @@ export {
QRCode,
RealtimeReport,
HoldExpense,
+ ReceiptFairy,
ReceiptEnvelope,
Approval,
WalletAlt,
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index 6ace3f5cdd37b..60404819bb02a 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -49,7 +49,7 @@ function ProcessMoneyRequestHoldMenu({onClose, onConfirm}: ProcessMoneyRequestHo
confirmText={translate('common.buttonConfirm')}
image={Illustrations.HoldExpense}
contentFitImage="cover"
- width={variables.holdEducationModalWidth}
+ width={variables.changePolicyEducationModalWidth}
illustrationAspectRatio={39 / 22}
contentInnerContainerStyles={styles.mb5}
modalInnerContainerStyle={styles.pt0}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 73da027938138..7a110e23f3724 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1094,6 +1094,13 @@ const translations = {
whatIsHoldExplain: 'Hold is like hitting “pause” on an expense to ask for more details before approval or payment.',
holdIsLeftBehind: 'Held expenses are left behind even if you approve an entire report.',
unholdWhenReady: "Unhold expenses when you're ready to approve or pay.",
+ changePolicyEducational: {
+ title: 'You moved this report!',
+ description: 'Double-check these items, which tend to change when moving reports to a new workspace.',
+ reCategorize: 'Re-categorize any expenses to comply with workspace rules.',
+ workflows: 'This report may now be subject to a different approval workflow.',
+ },
+ changeWorkspace: 'Change workspace',
set: 'set',
changed: 'changed',
removed: 'removed',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 0d54c36a90cf6..9c4e8c5997b42 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1091,6 +1091,13 @@ const translations = {
whatIsHoldExplain: 'Retener es como "pausar" un gasto para solicitar más detalles antes de aprobarlo o pagarlo.',
holdIsLeftBehind: 'Si apruebas un informe, los gastos retenidos se quedan fuera de esa aprobación.',
unholdWhenReady: 'Desbloquea los gastos cuando estés listo para aprobarlos o pagarlos.',
+ changePolicyEducational: {
+ title: 'You moved this report!',
+ description: 'Double-check these items, which tend to change when moving reports to a new workspace.',
+ reCategorize: 'Re-categorize any expenses to comply with workspace rules.',
+ workflows: 'This report may now be subject to a different approval workflow.',
+ },
+ changeWorkspace: 'Change workspace',
set: 'estableció',
changed: 'cambió',
removed: 'eliminó',
diff --git a/src/libs/API/parameters/ChangeReportPolicyParams.ts b/src/libs/API/parameters/ChangeReportPolicyParams.ts
new file mode 100644
index 0000000000000..d91e3409b699a
--- /dev/null
+++ b/src/libs/API/parameters/ChangeReportPolicyParams.ts
@@ -0,0 +1,7 @@
+type ChangeReportPolicyParams = {
+ reportID: string;
+ policyID: string;
+ reportPreviewReportActionID: string;
+ changePolicyReportActionID: string;
+};
+export default ChangeReportPolicyParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 38ee3ee710535..d63f32d506302 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -378,3 +378,4 @@ export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboar
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams';
export type {default as SaveCorpayOnboardingBeneficialOwnerParams} from './SaveCorpayOnboardingBeneficialOwnerParams';
+export type {default as ChangeReportPolicyParams} from './ChangeReportPolicyParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index c1a0341c71666..af91ff16ab0e1 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -461,6 +461,7 @@ const WRITE_COMMANDS = {
RESET_SMS_DELIVERY_FAILURE_STATUS: 'ResetSMSDeliveryFailureStatus',
SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS: 'SaveCorpayOnboardingCompanyDetails',
SAVE_CORPAY_ONBOARDING_BENEFICIAL_OWNER: 'SaveCorpayOnboardingBeneficialOwner',
+ CHANGE_REPORT_POLICY: 'ChangeReportPolicy',
} as const;
type WriteCommand = ValueOf;
@@ -933,6 +934,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY]: Parameters.JoinAccessiblePolicyParams;
// Dismis Product Training
[WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING]: Parameters.DismissProductTrainingParams;
+
+ // Change report policy
+ [WRITE_COMMANDS.CHANGE_REPORT_POLICY]: Parameters.ChangeReportPolicyParams;
};
const READ_COMMANDS = {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 91939c86f07fd..4016b548a4c4f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -18,6 +18,7 @@ import type {
ReimbursementAccountNavigatorParamList,
ReportDescriptionNavigatorParamList,
ReportDetailsNavigatorParamList,
+ ReportChangeWorkspaceNavigatorParamList,
ReportSettingsNavigatorParamList,
RoomMembersNavigatorParamList,
SearchAdvancedFiltersParamList,
@@ -134,6 +135,10 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator require('../../../../pages/home/report/ReportDetailsExportPage').default,
});
+const ReportChangeWorkspaceModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_CHANGE_WORKSPACE.ROOT]: () => require('../../../../pages/ReportChangeWorkspacePage').default,
+});
+
const ReportSettingsModalStackNavigator = createModalStackNavigator({
[SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default,
[SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default,
@@ -659,6 +664,10 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({
[SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: () => require('../../../../pages/ProcessMoneyRequestHoldPage').default,
});
+const ChangePolicyEducationalStackNavigator = createModalStackNavigator({
+ [SCREENS.CHANGE_POLICY_EDUCATIONAL_ROOT]: () => require('../../../../pages/ChangePolicyEducationalModal').default,
+});
+
const TransactionDuplicateStackNavigator = createModalStackNavigator({
[SCREENS.TRANSACTION_DUPLICATE.REVIEW]: () => require('../../../../pages/TransactionDuplicate/Review').default,
[SCREENS.TRANSACTION_DUPLICATE.MERCHANT]: () => require('../../../../pages/TransactionDuplicate/ReviewMerchant').default,
@@ -734,12 +743,14 @@ export {
NewTeachersUniteNavigator,
PrivateNotesModalStackNavigator,
ProcessMoneyRequestHoldStackNavigator,
+ ChangePolicyEducationalStackNavigator,
ProfileModalStackNavigator,
ReferralModalStackNavigator,
TravelModalStackNavigator,
ReimbursementAccountModalStackNavigator,
ReportDescriptionModalStackNavigator,
ReportDetailsModalStackNavigator,
+ ReportChangeWorkspaceModalStackNavigator,
ReportParticipantsModalStackNavigator,
ReportSettingsModalStackNavigator,
RoomMembersModalStackNavigator,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx
index db110d53bc633..74e182aacca3e 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx
@@ -7,6 +7,7 @@ import type {FeatureTrainingNavigatorParamList} from '@libs/Navigation/types';
import ProcessMoneyRequestHoldPage from '@pages/ProcessMoneyRequestHoldPage';
import TrackTrainingPage from '@pages/TrackTrainingPage';
import SCREENS from '@src/SCREENS';
+import ChangePolicyEducationalModal from '@pages/ChangePolicyEducationalModal';
const Stack = createPlatformStackNavigator();
@@ -23,6 +24,10 @@ function FeatureTrainingModalNavigator() {
name={SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT}
component={ProcessMoneyRequestHoldPage}
/>
+
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index b1d923c69b29c..3b5f96afc0909 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -112,6 +112,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.REPORT_DETAILS}
component={ModalStackNavigators.ReportDetailsModalStackNavigator}
/>
+
['config'] = {
exact: true,
},
[SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD.route,
+ [SCREENS.CHANGE_POLICY_EDUCATIONAL_ROOT]: ROUTES.CHANGE_POLICY_EDUCATIONAL.route,
},
},
[NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: {
@@ -1011,6 +1012,11 @@ const config: LinkingOptions['config'] = {
[SCREENS.REPORT_DETAILS.EXPORT]: ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.route,
},
},
+ [SCREENS.RIGHT_MODAL.REPORT_CHANGE_WORKSPACE]: {
+ screens: {
+ [SCREENS.REPORT_CHANGE_WORKSPACE.ROOT]: ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.route,
+ },
+ },
[SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: {
screens: {
[SCREENS.REPORT_SETTINGS.ROOT]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index fc73a6db8626a..e776360f851d8 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1029,6 +1029,13 @@ type ReportDetailsNavigatorParamList = {
};
};
+type ReportChangeWorkspaceNavigatorParamList = {
+ [SCREENS.REPORT_CHANGE_WORKSPACE.ROOT]: {
+ reportID: string;
+ backTo?: Routes;
+ };
+};
+
type ReportSettingsNavigatorParamList = {
[SCREENS.REPORT_SETTINGS.ROOT]: {
reportID: string;
@@ -1434,6 +1441,7 @@ type SignInNavigatorParamList = {
type FeatureTrainingNavigatorParamList = {
[SCREENS.FEATURE_TRAINING_ROOT]: undefined;
[SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: undefined;
+ [SCREENS.CHANGE_POLICY_EDUCATIONAL_ROOT]: undefined;
};
type ReferralDetailsNavigatorParamList = {
@@ -1501,6 +1509,7 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams;
[SCREENS.SETTINGS.SHARE_CODE]: undefined;
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.REPORT_CHANGE_WORKSPACE]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.SETTINGS_CATEGORIES]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.SETTINGS_TAGS]: NavigatorScreenParams;
@@ -1928,6 +1937,7 @@ export type {
ReimbursementAccountNavigatorParamList,
ReportDescriptionNavigatorParamList,
ReportDetailsNavigatorParamList,
+ ReportChangeWorkspaceNavigatorParamList,
ReportSettingsNavigatorParamList,
ReportsSplitNavigatorParamList,
RestrictedActionParamList,
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index f60a4bb2e668c..52254625bf4ea 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -1302,7 +1302,6 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) {
}
return [
CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD,
- CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY,
CONST.REPORT.ACTIONS.TYPE.CHANGE_TYPE,
CONST.REPORT.ACTIONS.TYPE.DELEGATE_SUBMIT,
CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_CSV,
@@ -1358,10 +1357,6 @@ function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldD
}
return translateLocal('report.actions.type.changeField', {oldValue, newValue, fieldName});
}
- case CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY: {
- const {fromPolicy, toPolicy} = originalMessage;
- return translateLocal('report.actions.type.changePolicy', {fromPolicy, toPolicy});
- }
case CONST.REPORT.ACTIONS.TYPE.DELEGATE_SUBMIT: {
const {delegateUser, originalManager} = originalMessage;
return translateLocal('report.actions.type.delegateSubmit', {delegateUser, originalManager});
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index dfa424ba5ef1e..c2320a92fb967 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -5670,6 +5670,54 @@ function buildOptimisticMovedReportAction(fromPolicyID: string | undefined, toPo
};
}
+/**
+ * Builds an optimistic CHANGEPOLICY report action with a randomly generated reportActionID.
+ * This action is used when we change the workspace of a report.
+ */
+function buildOptimisticChangePolicyReportAction(fromPolicyID: string | undefined, toPolicyID: string): ReportAction {
+ const originalMessage = {
+ fromPolicyID,
+ toPolicyID,
+ };
+
+ const fromPolicy = getPolicy(fromPolicyID);
+ const toPolicy = getPolicy(toPolicyID);
+
+ const changePolicyReportActionMessage = [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ text: `changed the workspace to ${toPolicy?.name}`,
+ },
+ ...(fromPolicyID
+ ? [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ text: `(previously ${fromPolicy?.name})`,
+ },
+ ]
+ : []),
+ ];
+
+ return {
+ actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY,
+ actorAccountID: currentUserAccountID,
+ avatar: getCurrentUserAvatar(),
+ created: DateUtils.getDBTime(),
+ originalMessage,
+ message: changePolicyReportActionMessage,
+ person: [
+ {
+ style: 'strong',
+ text: getCurrentUserDisplayNameOrEmail(),
+ type: 'TEXT',
+ },
+ ],
+ reportActionID: rand64(),
+ shouldShow: true,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ }
+}
+
/**
* Builds an optimistic SUBMITTED report action with a randomly generated reportActionID.
*
@@ -9536,6 +9584,7 @@ export {
isHiddenForCurrentUser,
prepareOnboardingOnyxData,
getReportSubtitlePrefix,
+ buildOptimisticChangePolicyReportAction,
};
export type {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 63448c89fc8aa..732de93944fc3 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -86,11 +86,13 @@ import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs
import {
buildOptimisticAddCommentReportAction,
buildOptimisticChangeFieldAction,
+ buildOptimisticChangePolicyReportAction,
buildOptimisticChatReport,
buildOptimisticCreatedReportAction,
buildOptimisticExportIntegrationAction,
buildOptimisticGroupChatReport,
buildOptimisticRenamedRoomReportAction,
+ buildOptimisticReportPreview,
buildOptimisticRoomDescriptionUpdatedReportAction,
buildOptimisticSelfDMReport,
buildOptimisticTaskCommentReportAction,
@@ -110,6 +112,7 @@ import {
getOriginalReportID,
getParsedComment,
getPendingChatMembers,
+ getPolicyExpenseChat,
getReportFieldKey,
getReportIDFromLink,
getReportLastMessage,
@@ -137,6 +140,7 @@ import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/NewRoomForm';
import type {
+ DismissedProductTraining,
IntroSelected,
InvitedEmailsToAccountIDs,
NewGroupChatDraft,
@@ -354,6 +358,12 @@ Onyx.connect({
callback: (value) => (allReportDraftComments = value),
});
+let nvpDismissedProductTraining: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
+ callback: (value) => (nvpDismissedProductTraining = value),
+});
+
let environmentURL: string;
Environment.getEnvironmentURL().then((url: string) => (environmentURL = url));
@@ -4667,6 +4677,184 @@ function clearDeleteTransactionNavigateBackUrl() {
Onyx.merge(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, null);
}
+
+function dismissChangePolicyModal() {
+ const date = new Date();
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
+ value: {
+ [CONST.CHANGE_POLICY_TRAINING_MODAL]: DateUtils.getDBTime(date.valueOf()),
+ },
+ },
+ ];
+ API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: CONST.CHANGE_POLICY_TRAINING_MODAL}, {optimisticData});
+}
+
+
+function changeReportPolicy(reportID: string, policyID: string){
+ if (!reportID || !policyID) {
+ return;
+ }
+ const reportToMove = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ if (!reportToMove || reportToMove?.policyID === policyID) {
+ return;
+ }
+
+ const optimisticData: OnyxUpdate[] = [];
+ const successData: OnyxUpdate[] = [];
+ const failureData: OnyxUpdate[] = [];
+
+ // 1. Optimistically set the policyID on the report (and all its threads)
+ function processReport(currentReportID: string) {
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${currentReportID}`];
+ const originalPolicyID = report?.policyID;
+
+ if (originalPolicyID) {
+
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${currentReportID}`,
+ value: {policyID},
+ });
+
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${currentReportID}`,
+ value: {policyID: originalPolicyID},
+ });
+ }
+
+ // Get child reports IDs for current report
+ const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportID}`] ?? {};
+ const childReportIDs = Object.values(reportActions)
+ .filter((action) => action?.childReportID)
+ .map((action) => action.childReportID?.toString());
+
+ // Recursively process child reports
+ childReportIDs.forEach((childReportID) => {
+ if (!childReportID) {
+ return;
+ }
+ processReport(childReportID);
+ });
+ }
+
+ // Start processing with the initial report
+ processReport(reportID);
+
+ // 2. If the old workspace had a workspace chat, mark the report preview action as deleted
+ if (reportToMove?.parentReportID && reportToMove?.parentReportActionID) {
+ const oldReportPreviewAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`]?.[reportToMove?.parentReportActionID];
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {[reportToMove?.parentReportActionID]: null},
+ });
+
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {[reportToMove?.parentReportActionID]: oldReportPreviewAction},
+ });
+ }
+
+ // 3. Optimistically create a new REPORTPREVIEW reportAction with the newReportPreviewActionID
+ // and set it as a parent of the moved report
+ const policyExpenseChat = getPolicyExpenseChat(currentUserAccountID, policyID);
+ const optimisticReportPreviewAction = buildOptimisticReportPreview(policyExpenseChat, reportToMove);
+
+ if (policyExpenseChat){
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat.reportID}`,
+ value: {[optimisticReportPreviewAction.reportActionID]: optimisticReportPreviewAction},
+ });
+ successData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat.reportID}`,
+ value: {
+ [optimisticReportPreviewAction.reportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
+ );
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat.reportID}`,
+ value: {[optimisticReportPreviewAction.reportActionID]: null},
+ });
+
+ // Set the new report preview action it as a parent of the moved report
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {parentReportActionID: optimisticReportPreviewAction.reportActionID},
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {parentReportActionID: reportToMove.parentReportActionID},
+ });
+
+ // Set lastVisibleActionCreated
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`,
+ value: {lastVisibleActionCreated: optimisticReportPreviewAction?.created},
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`,
+ value: {lastVisibleActionCreated: policyExpenseChat.lastVisibleActionCreated},
+ });
+ }
+
+ // 4. Optimistically create a CHANGEPOLICY reportAction on the report using the reportActionID
+ const optimisticMovedReportAction = buildOptimisticChangePolicyReportAction(reportToMove.policyID, policyID);
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove.reportID}`,
+ value: {[optimisticMovedReportAction.reportActionID]: optimisticMovedReportAction},
+ });
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove.reportID}`,
+ value: {
+ [optimisticMovedReportAction.reportActionID]: {
+ ...optimisticMovedReportAction,
+ pendingAction: null,
+ },
+ },
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove.reportID}`,
+ value: {[optimisticMovedReportAction.reportActionID]: null},
+ });
+
+ // 5. If the dismissedProductTraining.changeReportModal is not set,
+ // navigate to CHANGE_POLICY_EDUCATIONAL and a backTo param for the report page.
+ // Otherwise, return early
+
+ if(nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
+ return;
+ }
+ Navigation.navigate(ROUTES.CHANGE_POLICY_EDUCATIONAL.getRoute(ROUTES.REPORT_WITH_ID.getRoute(reportToMove.reportID)));
+
+ // Call the ChangeReportPolicy API endpoint
+ const params = {
+ reportID: reportToMove.reportID,
+ policyID,
+ reportPreviewReportActionID: optimisticReportPreviewAction.reportActionID,
+ changePolicyReportActionID: optimisticMovedReportAction.reportActionID,
+ };
+ API.write(WRITE_COMMANDS.CHANGE_REPORT_POLICY, params, {optimisticData, successData, failureData});
+}
+
export type {Video};
export {
@@ -4760,4 +4948,6 @@ export {
updateRoomVisibility,
updateWriteCapability,
prepareOnboardingOnyxData,
+ dismissChangePolicyModal,
+ changeReportPolicy,
};
diff --git a/src/pages/ChangePolicyEducationalModal.tsx b/src/pages/ChangePolicyEducationalModal.tsx
new file mode 100644
index 0000000000000..78e2d952a6f93
--- /dev/null
+++ b/src/pages/ChangePolicyEducationalModal.tsx
@@ -0,0 +1,36 @@
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useCallback, useRef} from 'react';
+import {InteractionManager} from 'react-native';
+import ChangePolicyEducationalMenu from '@components/ChangePolicyEducationalMenu';
+import blurActiveElement from '@libs/Accessibility/blurActiveElement';
+import {dismissChangePolicyModal} from '@libs/actions/Report';
+import CONST from '@src/CONST';
+
+function ChangePolicyEducationalModal() {
+ const focusTimeoutRef = useRef(null);
+ useFocusEffect(
+ useCallback(() => {
+ focusTimeoutRef.current = setTimeout(() => {
+ InteractionManager.runAfterInteractions(() => {
+ blurActiveElement();
+ });
+ }, CONST.ANIMATED_TRANSITION);
+ return () => focusTimeoutRef.current && clearTimeout(focusTimeoutRef.current);
+ }, []),
+ );
+
+ const onConfirm = useCallback(() => {
+ dismissChangePolicyModal();
+ }, []);
+
+ return (
+
+ );
+}
+
+ChangePolicyEducationalModal.displayName = 'ChangePolicyEducationalModal';
+
+export default ChangePolicyEducationalModal;
diff --git a/src/pages/ReportChangeWorkspacePage.tsx b/src/pages/ReportChangeWorkspacePage.tsx
new file mode 100644
index 0000000000000..b38357c8a2d71
--- /dev/null
+++ b/src/pages/ReportChangeWorkspacePage.tsx
@@ -0,0 +1,188 @@
+import React, {useCallback, useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import type {ListItem, SectionListDataType} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import useDebouncedState from '@hooks/useDebouncedState';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import {isPolicyAdmin, shouldShowPolicy, sortWorkspacesBySelected} from '@libs/PolicyUtils';
+import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
+import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils';
+import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import { changeReportPolicy } from '@libs/actions/Report';
+import type { ReportChangeWorkspaceNavigatorParamList } from '@libs/Navigation/types';
+import type SCREENS from '@src/SCREENS';
+import type { PlatformStackScreenProps } from '@libs/Navigation/PlatformStackNavigation/types';
+import type { WithReportOrNotFoundProps } from './home/report/withReportOrNotFound';
+import withReportOrNotFound from './home/report/withReportOrNotFound';
+
+type ReportChangeWorkspacePageProps = WithReportOrNotFoundProps & PlatformStackScreenProps;
+
+type WorkspaceListItem = {
+ text: string;
+ policyID?: string;
+ isPolicyAdmin?: boolean;
+ brickRoadIndicator?: BrickRoad;
+} & ListItem;
+
+function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
+ const reportID = report?.reportID;
+ const {isOffline} = useNetwork();
+ const styles = useThemeStyles();
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
+ const {translate} = useLocalize();
+ const {activeWorkspaceID} = useActiveWorkspace();
+
+ const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+ const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+ const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+ const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
+ const brickRoadsForPolicies = useMemo(() => getWorkspacesBrickRoads(reports, policies, reportActions), [reports, policies, reportActions]);
+ const unreadStatusesForPolicies = useMemo(() => getWorkspacesUnreadStatuses(reports), [reports]);
+ const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
+
+ const getIndicatorTypeForPolicy = useCallback(
+ (policyId?: string) => {
+ if (policyId && policyId !== activeWorkspaceID) {
+ return brickRoadsForPolicies[policyId];
+ }
+
+ if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)) {
+ return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
+ }
+
+ if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO)) {
+ return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
+ }
+
+ return undefined;
+ },
+ [activeWorkspaceID, brickRoadsForPolicies],
+ );
+
+ const hasUnreadData = useCallback(
+ // TO DO: Implement checking if policy has some unread data
+ (policyId?: string) => {
+ if (policyId) {
+ return unreadStatusesForPolicies[policyId];
+ }
+
+ return Object.values(unreadStatusesForPolicies).some((status) => status);
+ },
+ [unreadStatusesForPolicies],
+ );
+
+ const selectPolicy = useCallback(
+ (policyID?: string) => {
+ const newPolicyID = policyID === activeWorkspaceID ? undefined : policyID;
+
+ Navigation.goBack();
+ // On native platforms, we will see a blank screen if we navigate to a new HomeScreen route while navigating back at the same time.
+ // Therefore we delay switching the workspace until after back navigation, using the InteractionManager.
+ changeReportPolicy(reportID, newPolicyID ?? '');
+ },
+ [activeWorkspaceID, reportID],
+ );
+
+ const usersWorkspaces = useMemo(() => {
+ if (!policies || isEmptyObject(policies)) {
+ return [];
+ }
+
+ return Object.values(policies)
+ .filter((policy) => shouldShowPolicy(policy, !!isOffline, currentUserLogin) && !policy?.isJoinRequestPending)
+ .map((policy) => ({
+ text: policy?.name ?? '',
+ policyID: policy?.id,
+ brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id),
+ icons: [
+ {
+ source: policy?.avatarURL ? policy.avatarURL : getDefaultWorkspaceAvatar(policy?.name),
+ fallbackIcon: Expensicons.FallbackWorkspaceAvatar,
+ name: policy?.name,
+ type: CONST.ICON_TYPE_WORKSPACE,
+ id: policy?.id,
+ },
+ ],
+ isBold: hasUnreadData(policy?.id),
+ keyForList: policy?.id,
+ isPolicyAdmin: isPolicyAdmin(policy),
+ isSelected: activeWorkspaceID === policy?.id,
+ }));
+ }, [policies, isOffline, currentUserLogin, getIndicatorTypeForPolicy, hasUnreadData, activeWorkspaceID]);
+
+ const filteredAndSortedUserWorkspaces = useMemo(
+ () =>
+ usersWorkspaces
+ .filter((policy) => policy.text?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase() ?? ''))
+ .sort((policy1, policy2) => sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, activeWorkspaceID)),
+ [debouncedSearchTerm, usersWorkspaces, activeWorkspaceID],
+ );
+
+ const sections = useMemo(() => {
+ const options: Array> = [
+ {
+ data: filteredAndSortedUserWorkspaces,
+ shouldShow: true,
+ indexOffset: 1,
+ },
+ ];
+ return options;
+ }, [filteredAndSortedUserWorkspaces]);
+
+ const headerMessage = filteredAndSortedUserWorkspaces.length === 0 && usersWorkspaces.length ? translate('common.noResultsFound') : '';
+ const shouldShowCreateWorkspace = usersWorkspaces.length === 0;
+
+ return (
+
+ {({didScreenTransitionEnd}) => (
+ <>
+
+ {shouldShowLoadingIndicator ? (
+
+ ) : (
+
+ ListItem={UserListItem}
+ sections={sections}
+ onSelectRow={(option) => changeReportPolicy(reportID, option.policyID ?? '')}
+ textInputLabel={usersWorkspaces.length >= CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined}
+ textInputValue={searchTerm}
+ onChangeText={setSearchTerm}
+ headerMessage={headerMessage}
+ shouldShowListEmptyContent={shouldShowCreateWorkspace}
+ initiallyFocusedOptionKey={activeWorkspaceID ?? CONST.WORKSPACE_SWITCHER.NAME}
+ showLoadingPlaceholder={fetchStatus.status === 'loading' || !didScreenTransitionEnd}
+ showConfirmButton={!!activeWorkspaceID}
+ shouldUseDefaultTheme
+ confirmButtonText={translate('workspace.common.clearFilter')}
+ onConfirm={() => selectPolicy(undefined)}
+ />
+ )}
+ >
+ )}
+
+ );
+}
+
+ReportChangeWorkspacePage.displayName = 'ReportChangeWorkspacePage';
+
+export default withReportOrNotFound()(ReportChangeWorkspacePage);
diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx
index 55fc168d3d7c0..b361f4e28f1ac 100644
--- a/src/pages/home/report/withReportOrNotFound.tsx
+++ b/src/pages/home/report/withReportOrNotFound.tsx
@@ -11,6 +11,7 @@ import * as ReportUtils from '@libs/ReportUtils';
import type {
ParticipantsNavigatorParamList,
PrivateNotesNavigatorParamList,
+ ReportChangeWorkspaceNavigatorParamList,
ReportDescriptionNavigatorParamList,
ReportDetailsNavigatorParamList,
ReportSettingsNavigatorParamList,
@@ -49,7 +50,8 @@ type ScreenProps =
| PlatformStackScreenProps
| PlatformStackScreenProps
| PlatformStackScreenProps
- | PlatformStackScreenProps;
+ | PlatformStackScreenProps
+ | PlatformStackScreenProps;
type WithReportOrNotFoundProps = WithReportOrNotFoundOnyxProps & {
route: ScreenProps['route'];
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index f087a9a193735..ff986dc40abe6 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -212,6 +212,7 @@ export default {
photoUploadPopoverWidth: 335,
onboardingModalWidth: 500,
holdEducationModalWidth: 400,
+ changePolicyEducationModalWidth: 400,
fontSizeToWidthRatio: getValueUsingPixelRatio(0.8, 1),
// Emoji related variables
diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts
index 0ab9425398bd9..105dd7ba00392 100644
--- a/src/types/onyx/DismissedProductTraining.ts
+++ b/src/types/onyx/DismissedProductTraining.ts
@@ -64,6 +64,13 @@ type DismissedProductTraining = {
* When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here.
*/
[SCAN_TEST_TOOLTIP]: string;
+
+ /**
+ * When user dismisses the ChangeReportPolicy feature training modal, we store the timestamp here.
+ */
+ [CONST.CHANGE_POLICY_TRAINING_MODAL]: string;
+
+
};
export default DismissedProductTraining;
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index d123cf646dda9..d67a05c9a45c5 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -541,6 +541,15 @@ type OriginalMessageReimbursementDequeued = {
currency: string;
};
+/** Model of `CHANGEPOLICY` report action */
+type OriginalMessageChangePolicy = {
+ /** ID of the old policy */
+ fromPolicyID: string | undefined;
+
+ /** ID of the new policy */
+ toPolicyID: string;
+};
+
/** Model of `moved` report action */
type OriginalMessageMoved = {
/** ID of the old policy */
@@ -726,7 +735,7 @@ type OriginalMessageMap = {
[CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT]: OriginalMessageAddComment;
[CONST.REPORT.ACTIONS.TYPE.APPROVED]: OriginalMessageApproved;
[CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD]: never;
- [CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY]: never;
+ [CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY]: OriginalMessageChangePolicy;
[CONST.REPORT.ACTIONS.TYPE.CHANGE_TYPE]: never;
[CONST.REPORT.ACTIONS.TYPE.CHRONOS_OOO_LIST]: OriginalMessageChronosOOOList;
[CONST.REPORT.ACTIONS.TYPE.CLOSED]: OriginalMessageClosed;
@@ -811,4 +820,5 @@ export type {
OriginalMessageModifiedExpense,
OriginalMessageExportIntegration,
IssueNewCardOriginalMessage,
+ OriginalMessageChangePolicy,
};
From 0503fcfd47bdb08f66fb1b813df7dafa6aa642c1 Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 18:05:07 +0100
Subject: [PATCH 02/30] Correct ModalStackNavigator name
---
.../Navigation/AppNavigator/Navigators/RightModalNavigator.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 3b5f96afc0909..7632b0fefa008 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -114,7 +114,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
/>
Date: Fri, 28 Feb 2025 18:20:53 +0100
Subject: [PATCH 03/30] Fix changeReportPolicy action logic
---
src/libs/actions/Report.ts | 20 +++++++-------------
1 file changed, 7 insertions(+), 13 deletions(-)
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 732de93944fc3..a8913877dd9ac 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -4707,18 +4707,16 @@ function changeReportPolicy(reportID: string, policyID: string){
const failureData: OnyxUpdate[] = [];
// 1. Optimistically set the policyID on the report (and all its threads)
- function processReport(currentReportID: string) {
+ function updatePolicyIdForReportAndThreads(currentReportID: string) {
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${currentReportID}`];
const originalPolicyID = report?.policyID;
if (originalPolicyID) {
-
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${currentReportID}`,
value: {policyID},
});
-
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${currentReportID}`,
@@ -4737,25 +4735,24 @@ function changeReportPolicy(reportID: string, policyID: string){
if (!childReportID) {
return;
}
- processReport(childReportID);
+ updatePolicyIdForReportAndThreads(childReportID);
});
}
// Start processing with the initial report
- processReport(reportID);
+ updatePolicyIdForReportAndThreads(reportID);
// 2. If the old workspace had a workspace chat, mark the report preview action as deleted
if (reportToMove?.parentReportID && reportToMove?.parentReportActionID) {
const oldReportPreviewAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`]?.[reportToMove?.parentReportActionID];
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`,
value: {[reportToMove?.parentReportActionID]: null},
});
-
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`,
value: {[reportToMove?.parentReportActionID]: oldReportPreviewAction},
});
}
@@ -4838,12 +4835,9 @@ function changeReportPolicy(reportID: string, policyID: string){
// 5. If the dismissedProductTraining.changeReportModal is not set,
// navigate to CHANGE_POLICY_EDUCATIONAL and a backTo param for the report page.
- // Otherwise, return early
-
- if(nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
- return;
+ if(!nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
+ Navigation.navigate(ROUTES.CHANGE_POLICY_EDUCATIONAL.getRoute(ROUTES.REPORT_WITH_ID.getRoute(reportToMove.reportID)));;
}
- Navigation.navigate(ROUTES.CHANGE_POLICY_EDUCATIONAL.getRoute(ROUTES.REPORT_WITH_ID.getRoute(reportToMove.reportID)));
// Call the ChangeReportPolicy API endpoint
const params = {
From 5f7bd16c4ea779dbd10f8bf2752af27b880d0e3d Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 18:26:21 +0100
Subject: [PATCH 04/30] add comments to functions
---
src/libs/actions/Report.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index a8913877dd9ac..a9cfa49e12b2f 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -4678,6 +4678,9 @@ function clearDeleteTransactionNavigateBackUrl() {
}
+/**
+ * Dismisses the change report's policy educational modal so that it doesn't show up again.
+ */
function dismissChangePolicyModal() {
const date = new Date();
const optimisticData = [
@@ -4693,6 +4696,9 @@ function dismissChangePolicyModal() {
}
+/**
+ * Changes the policy of a report and all its child reports, and moves the report to the new policy's workspace chat.
+ */
function changeReportPolicy(reportID: string, policyID: string){
if (!reportID || !policyID) {
return;
@@ -4835,7 +4841,7 @@ function changeReportPolicy(reportID: string, policyID: string){
// 5. If the dismissedProductTraining.changeReportModal is not set,
// navigate to CHANGE_POLICY_EDUCATIONAL and a backTo param for the report page.
- if(!nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
+ if (!nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
Navigation.navigate(ROUTES.CHANGE_POLICY_EDUCATIONAL.getRoute(ROUTES.REPORT_WITH_ID.getRoute(reportToMove.reportID)));;
}
From 43377ba790b0133fd1cd3fcb531a91815a215ed6 Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 18:30:32 +0100
Subject: [PATCH 05/30] revert a change
---
src/components/ProcessMoneyRequestHoldMenu.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index 60404819bb02a..6ace3f5cdd37b 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -49,7 +49,7 @@ function ProcessMoneyRequestHoldMenu({onClose, onConfirm}: ProcessMoneyRequestHo
confirmText={translate('common.buttonConfirm')}
image={Illustrations.HoldExpense}
contentFitImage="cover"
- width={variables.changePolicyEducationModalWidth}
+ width={variables.holdEducationModalWidth}
illustrationAspectRatio={39 / 22}
contentInnerContainerStyles={styles.mb5}
modalInnerContainerStyle={styles.pt0}
From cfc5f079038f26f88ae75ec6dc7366d53469b860 Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 19:01:48 +0100
Subject: [PATCH 06/30] ReportChangeWorkspacePage
---
src/libs/actions/Report.ts | 14 ++---
src/pages/ReportChangeWorkspacePage.tsx | 69 +++----------------------
2 files changed, 16 insertions(+), 67 deletions(-)
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index a9cfa49e12b2f..d37c3a87983f1 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -4839,12 +4839,6 @@ function changeReportPolicy(reportID: string, policyID: string){
value: {[optimisticMovedReportAction.reportActionID]: null},
});
- // 5. If the dismissedProductTraining.changeReportModal is not set,
- // navigate to CHANGE_POLICY_EDUCATIONAL and a backTo param for the report page.
- if (!nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
- Navigation.navigate(ROUTES.CHANGE_POLICY_EDUCATIONAL.getRoute(ROUTES.REPORT_WITH_ID.getRoute(reportToMove.reportID)));;
- }
-
// Call the ChangeReportPolicy API endpoint
const params = {
reportID: reportToMove.reportID,
@@ -4853,6 +4847,14 @@ function changeReportPolicy(reportID: string, policyID: string){
changePolicyReportActionID: optimisticMovedReportAction.reportActionID,
};
API.write(WRITE_COMMANDS.CHANGE_REPORT_POLICY, params, {optimisticData, successData, failureData});
+
+ // 5. If the dismissedProductTraining.changeReportModal is not set,
+ // navigate to CHANGE_POLICY_EDUCATIONAL and a backTo param for the report page.
+ if (!nvpDismissedProductTraining?.[CONST.CHANGE_POLICY_TRAINING_MODAL]){
+ Navigation.navigate(ROUTES.CHANGE_POLICY_EDUCATIONAL.getRoute(ROUTES.REPORT_WITH_ID.getRoute(reportToMove.reportID)));
+ return;
+ }
+ Navigation.goBack();
}
export type {Video};
diff --git a/src/pages/ReportChangeWorkspacePage.tsx b/src/pages/ReportChangeWorkspacePage.tsx
index b38357c8a2d71..db079a33292b9 100644
--- a/src/pages/ReportChangeWorkspacePage.tsx
+++ b/src/pages/ReportChangeWorkspacePage.tsx
@@ -7,7 +7,6 @@ import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import type {ListItem, SectionListDataType} from '@components/SelectionList/types';
import UserListItem from '@components/SelectionList/UserListItem';
-import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -15,8 +14,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import {isPolicyAdmin, shouldShowPolicy, sortWorkspacesBySelected} from '@libs/PolicyUtils';
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
-import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils';
-import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -33,7 +30,6 @@ type WorkspaceListItem = {
text: string;
policyID?: string;
isPolicyAdmin?: boolean;
- brickRoadIndicator?: BrickRoad;
} & ListItem;
function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
@@ -42,58 +38,17 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
const styles = useThemeStyles();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const {translate} = useLocalize();
- const {activeWorkspaceID} = useActiveWorkspace();
- const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
- const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
- const brickRoadsForPolicies = useMemo(() => getWorkspacesBrickRoads(reports, policies, reportActions), [reports, policies, reportActions]);
- const unreadStatusesForPolicies = useMemo(() => getWorkspacesUnreadStatuses(reports), [reports]);
const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
- const getIndicatorTypeForPolicy = useCallback(
- (policyId?: string) => {
- if (policyId && policyId !== activeWorkspaceID) {
- return brickRoadsForPolicies[policyId];
- }
-
- if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)) {
- return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
- }
-
- if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO)) {
- return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
- }
-
- return undefined;
- },
- [activeWorkspaceID, brickRoadsForPolicies],
- );
-
- const hasUnreadData = useCallback(
- // TO DO: Implement checking if policy has some unread data
- (policyId?: string) => {
- if (policyId) {
- return unreadStatusesForPolicies[policyId];
- }
-
- return Object.values(unreadStatusesForPolicies).some((status) => status);
- },
- [unreadStatusesForPolicies],
- );
-
const selectPolicy = useCallback(
(policyID?: string) => {
- const newPolicyID = policyID === activeWorkspaceID ? undefined : policyID;
-
- Navigation.goBack();
- // On native platforms, we will see a blank screen if we navigate to a new HomeScreen route while navigating back at the same time.
- // Therefore we delay switching the workspace until after back navigation, using the InteractionManager.
- changeReportPolicy(reportID, newPolicyID ?? '');
+ changeReportPolicy(reportID, policyID ?? '');
},
- [activeWorkspaceID, reportID],
+ [reportID],
);
const usersWorkspaces = useMemo(() => {
@@ -106,7 +61,6 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
.map((policy) => ({
text: policy?.name ?? '',
policyID: policy?.id,
- brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id),
icons: [
{
source: policy?.avatarURL ? policy.avatarURL : getDefaultWorkspaceAvatar(policy?.name),
@@ -116,19 +70,18 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
id: policy?.id,
},
],
- isBold: hasUnreadData(policy?.id),
keyForList: policy?.id,
isPolicyAdmin: isPolicyAdmin(policy),
- isSelected: activeWorkspaceID === policy?.id,
+ isSelected: report.policyID === policy?.id,
}));
- }, [policies, isOffline, currentUserLogin, getIndicatorTypeForPolicy, hasUnreadData, activeWorkspaceID]);
+ }, [policies, isOffline, currentUserLogin, report.policyID]);
const filteredAndSortedUserWorkspaces = useMemo(
() =>
usersWorkspaces
.filter((policy) => policy.text?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase() ?? ''))
- .sort((policy1, policy2) => sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, activeWorkspaceID)),
- [debouncedSearchTerm, usersWorkspaces, activeWorkspaceID],
+ .sort((policy1, policy2) => sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, report.policyID)),
+ [debouncedSearchTerm, usersWorkspaces, report.policyID],
);
const sections = useMemo(() => {
@@ -143,7 +96,6 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
}, [filteredAndSortedUserWorkspaces]);
const headerMessage = filteredAndSortedUserWorkspaces.length === 0 && usersWorkspaces.length ? translate('common.noResultsFound') : '';
- const shouldShowCreateWorkspace = usersWorkspaces.length === 0;
return (
ListItem={UserListItem}
sections={sections}
- onSelectRow={(option) => changeReportPolicy(reportID, option.policyID ?? '')}
+ onSelectRow={(option) => selectPolicy(option.policyID ?? '')}
textInputLabel={usersWorkspaces.length >= CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined}
textInputValue={searchTerm}
onChangeText={setSearchTerm}
headerMessage={headerMessage}
- shouldShowListEmptyContent={shouldShowCreateWorkspace}
- initiallyFocusedOptionKey={activeWorkspaceID ?? CONST.WORKSPACE_SWITCHER.NAME}
+ initiallyFocusedOptionKey={report.policyID}
showLoadingPlaceholder={fetchStatus.status === 'loading' || !didScreenTransitionEnd}
- showConfirmButton={!!activeWorkspaceID}
- shouldUseDefaultTheme
- confirmButtonText={translate('workspace.common.clearFilter')}
- onConfirm={() => selectPolicy(undefined)}
/>
)}
>
From d285d3e0771122aeb93d9f628f24f4c23f1a966a Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 21:00:13 +0100
Subject: [PATCH 07/30] isWorkspaceEligibleForReportChange
---
src/libs/PolicyUtils.ts | 24 ++++++++++++++++++++++++
src/pages/ReportChangeWorkspacePage.tsx | 21 +++++++++++++--------
2 files changed, 37 insertions(+), 8 deletions(-)
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 92f47b91e8ee3..669d8b31b6114 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1087,6 +1087,29 @@ const sortWorkspacesBySelected = (workspace1: WorkspaceDetails, workspace2: Work
return workspace1.name?.toLowerCase().localeCompare(workspace2.name?.toLowerCase() ?? '') ?? 0;
};
+/**
+ * An eligible workspace is one that meets the following criteria:
+ * Submitters: workspaces where the submitter is a member of
+ * Approvers: workspaces where both the approver AND submitter are members of
+ * Admins: same as approvers OR workspaces where the admin is an admin of (note that the submitter is invited to the workspace in this case)
+ */
+const isWorkspaceEligibleForReportChange = (
+ newPolicy: OnyxEntry,
+ reportOwnerAccountID: number | undefined,
+ reportManagerID: number | undefined,
+ currentUserLogin: string | undefined,
+): boolean => {
+ const curretUserAccountID = getCurrentUserAccountID();
+ if (curretUserAccountID === reportOwnerAccountID) {
+ return !!currentUserLogin && !!newPolicy?.employeeList?.[currentUserLogin];
+ }
+ if (curretUserAccountID === reportManagerID) {
+ const reportSubmitterLogin = (!!reportOwnerAccountID && getLoginsByAccountIDs([reportOwnerAccountID]).at(0)) ?? '';
+ return !!currentUserLogin && !!newPolicy?.employeeList?.[currentUserLogin] && !!reportSubmitterLogin && !!newPolicy?.employeeList?.[reportSubmitterLogin];
+ }
+ return isUserPolicyAdmin(newPolicy, currentUserLogin);
+};
+
/**
* Takes removes pendingFields and errorFields from a customUnit
*/
@@ -1492,6 +1515,7 @@ export {
getPolicyNameByID,
getMostFrequentEmailDomain,
getDescriptionForPolicyDomainCard,
+ isWorkspaceEligibleForReportChange,
};
export type {MemberEmailsToAccountIDs};
diff --git a/src/pages/ReportChangeWorkspacePage.tsx b/src/pages/ReportChangeWorkspacePage.tsx
index db079a33292b9..057e6dd62fdc8 100644
--- a/src/pages/ReportChangeWorkspacePage.tsx
+++ b/src/pages/ReportChangeWorkspacePage.tsx
@@ -11,17 +11,17 @@ import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
+import {changeReportPolicy} from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
-import {isPolicyAdmin, shouldShowPolicy, sortWorkspacesBySelected} from '@libs/PolicyUtils';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {ReportChangeWorkspaceNavigatorParamList} from '@libs/Navigation/types';
+import {isPolicyAdmin, isWorkspaceEligibleForReportChange, shouldShowPolicy, sortWorkspacesBySelected} from '@libs/PolicyUtils';
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import { changeReportPolicy } from '@libs/actions/Report';
-import type { ReportChangeWorkspaceNavigatorParamList } from '@libs/Navigation/types';
import type SCREENS from '@src/SCREENS';
-import type { PlatformStackScreenProps } from '@libs/Navigation/PlatformStackNavigation/types';
-import type { WithReportOrNotFoundProps } from './home/report/withReportOrNotFound';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound';
import withReportOrNotFound from './home/report/withReportOrNotFound';
type ReportChangeWorkspacePageProps = WithReportOrNotFoundProps & PlatformStackScreenProps;
@@ -57,7 +57,12 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
}
return Object.values(policies)
- .filter((policy) => shouldShowPolicy(policy, !!isOffline, currentUserLogin) && !policy?.isJoinRequestPending)
+ .filter(
+ (policy) =>
+ shouldShowPolicy(policy, !!isOffline, currentUserLogin) &&
+ !policy?.isJoinRequestPending &&
+ isWorkspaceEligibleForReportChange(policy, report?.ownerAccountID, report?.managerID, currentUserLogin),
+ )
.map((policy) => ({
text: policy?.name ?? '',
policyID: policy?.id,
@@ -74,7 +79,7 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
isPolicyAdmin: isPolicyAdmin(policy),
isSelected: report.policyID === policy?.id,
}));
- }, [policies, isOffline, currentUserLogin, report.policyID]);
+ }, [policies, isOffline, currentUserLogin, report.policyID, report?.ownerAccountID, report?.managerID]);
const filteredAndSortedUserWorkspaces = useMemo(
() =>
From c80610c8c48683b51b1aa206cce7fa0b2c195cc5 Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 21:38:02 +0100
Subject: [PATCH 08/30] action message
---
src/languages/en.ts | 2 ++
src/languages/es.ts | 2 ++
src/languages/params.ts | 3 ++
src/libs/ReportUtils.ts | 33 ++++++++++++-------
.../home/report/PureReportActionItem.tsx | 3 ++
5 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 24ae33ad6ed1b..024c82d49ffb5 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -38,6 +38,7 @@ import type {
ChangeOwnerHasFailedSettlementsParams,
ChangeOwnerSubscriptionParams,
ChangePolicyParams,
+ ChangeReportPolicyParams,
ChangeTypeParams,
CharacterLengthLimitParams,
CharacterLimitParams,
@@ -5149,6 +5150,7 @@ const translations = {
changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} from ${oldValue} to ${newValue}`,
changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} to ${newValue}`,
changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `changed the workspace to ${toPolicy} (previously ${fromPolicy})`,
+ changeReportPolicy: ({fromPolicyName, toPolicyName}: ChangeReportPolicyParams) => `changed the workspace to ${toPolicyName}${fromPolicyName ? ` (previously ${fromPolicyName}`: ''})`,
changeType: ({oldType, newType}: ChangeTypeParams) => `changed type from ${oldType} to ${newType}`,
delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `sent this report to ${delegateUser} since ${originalManager} is on vacation`,
exportedToCSV: `exported this report to CSV`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 714f1714da61f..5ab52bd27dd6d 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -37,6 +37,7 @@ import type {
ChangeOwnerHasFailedSettlementsParams,
ChangeOwnerSubscriptionParams,
ChangePolicyParams,
+ ChangeReportPolicyParams,
ChangeTypeParams,
CharacterLengthLimitParams,
CharacterLimitParams,
@@ -5204,6 +5205,7 @@ const translations = {
changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `cambió ${fieldName} de ${oldValue} a ${newValue}`,
changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `cambió ${fieldName} a ${newValue}`,
changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `cambió el espacio de trabajo a ${toPolicy} (previamente ${fromPolicy})`,
+ changeReportPolicy: ({fromPolicyName, toPolicyName}: ChangeReportPolicyParams) => `changed the workspace to ${toPolicyName}${fromPolicyName ? ` (previously ${fromPolicyName}`: ''})`,
changeType: ({oldType, newType}: ChangeTypeParams) => `cambió type de ${oldType} a ${newType}`,
delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `envié este informe a ${delegateUser} ya que ${originalManager} está de vacaciones`,
exportedToCSV: `exportó este informe a CSV`,
diff --git a/src/languages/params.ts b/src/languages/params.ts
index 2b5458fc047c8..dba65e45efbf6 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -293,6 +293,8 @@ type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string
type ChangePolicyParams = {fromPolicy: string; toPolicy: string};
+type ChangeReportPolicyParams = {fromPolicyName?: string; toPolicyName: string};
+
type UpdatedPolicyDescriptionParams = {oldDescription: string; newDescription: string};
type UpdatedPolicyCurrencyParams = {oldCurrency: string; newCurrency: string};
@@ -828,6 +830,7 @@ export type {
ZipCodeExampleFormatParams,
ChangeFieldParams,
ChangePolicyParams,
+ ChangeReportPolicyParams,
ChangeTypeParams,
ExportedToIntegrationParams,
DelegateSubmitParams,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 4d0dafa867e41..ecde1af0b7eaa 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -9,7 +9,7 @@ import lodashMaxBy from 'lodash/maxBy';
import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {SvgProps} from 'react-native-svg';
-import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage';
+import type {OriginalMessageChangePolicy, OriginalMessageIOU, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage';
import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest';
import type {FileObject} from '@components/AttachmentModal';
import {FallbackAvatar, IntacctSquare, NetSuiteSquare, NSQSSquare, QBOSquare, XeroSquare} from '@components/Icon/Expensicons';
@@ -91,6 +91,7 @@ import {
getForwardsToAccount,
getManagerAccountEmail,
getPolicyEmployeeListByIdWithoutCurrentUser,
+ getPolicyNameByID,
getRuleApprovers,
getSubmitToAccountID,
isExpensifyTeam,
@@ -5394,6 +5395,13 @@ function getDeletedTransactionMessage(action: ReportAction) {
return message;
}
+function getPolicyChangeMessage(action: ReportAction) {
+ const PolicyChangeOriginalMessage = getOriginalMessage(action as ReportAction) ?? {};
+ const {fromPolicyID, toPolicyID} = PolicyChangeOriginalMessage as OriginalMessageChangePolicy;
+ const message = translateLocal('report.actions.type.changeReportPolicy', {fromPolicyName: getPolicyNameByID(fromPolicyID ?? ''), toPolicyName: getPolicyNameByID(toPolicyID)});
+ return message;
+}
+
/**
* @param iouReportID - the report ID of the IOU report the action belongs to
* @param type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split)
@@ -5685,18 +5693,18 @@ function buildOptimisticChangePolicyReportAction(fromPolicyID: string | undefine
const toPolicy = getPolicy(toPolicyID);
const changePolicyReportActionMessage = [
- {
+ {
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
text: `changed the workspace to ${toPolicy?.name}`,
- },
- ...(fromPolicyID
- ? [
- {
- type: CONST.REPORT.MESSAGE.TYPE.TEXT,
- text: `(previously ${fromPolicy?.name})`,
- },
- ]
- : []),
+ },
+ ...(fromPolicyID
+ ? [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ text: `(previously ${fromPolicy?.name})`,
+ },
+ ]
+ : []),
];
return {
@@ -5716,7 +5724,7 @@ function buildOptimisticChangePolicyReportAction(fromPolicyID: string | undefine
reportActionID: rand64(),
shouldShow: true,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- }
+ };
}
/**
@@ -9586,6 +9594,7 @@ export {
prepareOnboardingOnyxData,
getReportSubtitlePrefix,
buildOptimisticChangePolicyReportAction,
+ getPolicyChangeMessage,
};
export type {
diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx
index 8ef1fe4a94426..60741bf5b1156 100644
--- a/src/pages/home/report/PureReportActionItem.tsx
+++ b/src/pages/home/report/PureReportActionItem.tsx
@@ -110,6 +110,7 @@ import {
getIOUForwardedMessage,
getIOUSubmittedMessage,
getIOUUnapprovedMessage,
+ getPolicyChangeMessage,
getReportAutomaticallyApprovedMessage,
getReportAutomaticallySubmittedMessage,
getWhisperDisplayNames,
@@ -877,6 +878,8 @@ function PureReportActionItem({
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY) {
+ children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) {
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION) {
From 706c8e23a40ac173fdf2bb0fde20c62c1e90d46c Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Fri, 28 Feb 2025 21:43:46 +0100
Subject: [PATCH 09/30] remove unused translation
---
src/languages/en.ts | 1 -
src/languages/es.ts | 3 +--
src/languages/params.ts | 3 ---
3 files changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 024c82d49ffb5..62205024d1c2f 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -5149,7 +5149,6 @@ const translations = {
type: {
changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} from ${oldValue} to ${newValue}`,
changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} to ${newValue}`,
- changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `changed the workspace to ${toPolicy} (previously ${fromPolicy})`,
changeReportPolicy: ({fromPolicyName, toPolicyName}: ChangeReportPolicyParams) => `changed the workspace to ${toPolicyName}${fromPolicyName ? ` (previously ${fromPolicyName}`: ''})`,
changeType: ({oldType, newType}: ChangeTypeParams) => `changed type from ${oldType} to ${newType}`,
delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `sent this report to ${delegateUser} since ${originalManager} is on vacation`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5ab52bd27dd6d..52bdab8923dca 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -5204,8 +5204,7 @@ const translations = {
type: {
changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `cambió ${fieldName} de ${oldValue} a ${newValue}`,
changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `cambió ${fieldName} a ${newValue}`,
- changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `cambió el espacio de trabajo a ${toPolicy} (previamente ${fromPolicy})`,
- changeReportPolicy: ({fromPolicyName, toPolicyName}: ChangeReportPolicyParams) => `changed the workspace to ${toPolicyName}${fromPolicyName ? ` (previously ${fromPolicyName}`: ''})`,
+ changeReportPolicy: ({fromPolicyName, toPolicyName}: ChangeReportPolicyParams) => `cambió el espacio de trabajo a ${toPolicyName}${fromPolicyName ? ` (previamente ${fromPolicyName}`: ''})`,
changeType: ({oldType, newType}: ChangeTypeParams) => `cambió type de ${oldType} a ${newType}`,
delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `envié este informe a ${delegateUser} ya que ${originalManager} está de vacaciones`,
exportedToCSV: `exportó este informe a CSV`,
diff --git a/src/languages/params.ts b/src/languages/params.ts
index dba65e45efbf6..f41273eb6e767 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -291,8 +291,6 @@ type HeldRequestParams = {comment: string};
type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string};
-type ChangePolicyParams = {fromPolicy: string; toPolicy: string};
-
type ChangeReportPolicyParams = {fromPolicyName?: string; toPolicyName: string};
type UpdatedPolicyDescriptionParams = {oldDescription: string; newDescription: string};
@@ -829,7 +827,6 @@ export type {
WelcomeToRoomParams,
ZipCodeExampleFormatParams,
ChangeFieldParams,
- ChangePolicyParams,
ChangeReportPolicyParams,
ChangeTypeParams,
ExportedToIntegrationParams,
From cebcba7beb0a1d07c1aa29d0d07fa8a8f785184a Mon Sep 17 00:00:00 2001
From: Rayane <77965000+rayane-d@users.noreply.github.com>
Date: Sat, 1 Mar 2025 00:37:50 +0100
Subject: [PATCH 10/30] ChangePolicyEducationalModal
---
.../emptystate__receiptfairy.svg | 307 +++++++++---------
.../ChangePolicyEducationalMenu.tsx | 18 +-
.../ChangeWorkspaceMenuSectionList.tsx | 9 +-
src/components/FeatureTrainingModal.tsx | 10 +
src/styles/variables.ts | 3 +
5 files changed, 185 insertions(+), 162 deletions(-)
diff --git a/assets/images/product-illustrations/emptystate__receiptfairy.svg b/assets/images/product-illustrations/emptystate__receiptfairy.svg
index 2c25f7dd11f1e..ccdeda5926f89 100644
--- a/assets/images/product-illustrations/emptystate__receiptfairy.svg
+++ b/assets/images/product-illustrations/emptystate__receiptfairy.svg
@@ -1,155 +1,154 @@
-