diff --git a/src/CONST.ts b/src/CONST.ts
index 829a26c5cb96..3327a8ad88b6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -5471,6 +5471,18 @@ const CONST = {
NAVIGATION_ACTIONS: {
RESET: 'RESET',
},
+
+ APPROVAL_WORKFLOW: {
+ ACTION: {
+ CREATE: 'create',
+ EDIT: 'edit',
+ },
+ TYPE: {
+ CREATE: 'create',
+ UPDATE: 'update',
+ REMOVE: 'remove',
+ },
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index acfa3a70c032..8c339e9120ab 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -884,7 +884,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number;
[ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip;
[ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[];
- [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow;
+ [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index d525cc0ee790..0fa1d30f1b03 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -633,12 +633,12 @@ const ROUTES = {
},
WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM: {
route: 'settings/workspaces/:policyID/workflows/approvals/expenses-from',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows/approvals/expenses-from` as const,
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/workflows/approvals/expenses-from` as const, backTo),
},
WORKSPACE_WORKFLOWS_APPROVALS_APPROVER: {
route: 'settings/workspaces/:policyID/workflows/approvals/approver',
- getRoute: (policyID: string, approverIndex?: number) =>
- `settings/workspaces/${policyID}/workflows/approvals/approver${approverIndex !== undefined ? `?approverIndex=${approverIndex}` : ''}` as const,
+ getRoute: (policyID: string, approverIndex?: number, backTo?: string) =>
+ getUrlWithBackToParam(`settings/workspaces/${policyID}/workflows/approvals/approver${approverIndex !== undefined ? `?approverIndex=${approverIndex}` : ''}` as const, backTo),
},
WORKSPACE_WORKFLOWS_PAYER: {
route: 'settings/workspaces/:policyID/workflows/payer',
diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx
index 899e83c9440b..49e509ce533e 100644
--- a/src/components/ApprovalWorkflowSection.tsx
+++ b/src/components/ApprovalWorkflowSection.tsx
@@ -18,17 +18,17 @@ type ApprovalWorkflowSectionProps = {
approvalWorkflow: ApprovalWorkflow;
/** ID of the policy */
- policyId?: string;
+ policyID: string;
};
-function ApprovalWorkflowSection({approvalWorkflow, policyId}: ApprovalWorkflowSectionProps) {
+function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowSectionProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate, toLocaleOrdinal} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
const openApprovalsEdit = useCallback(
- () => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyId ?? '', approvalWorkflow.approvers[0].email)),
- [approvalWorkflow.approvers, policyId],
+ () => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, approvalWorkflow.approvers[0].email)),
+ [approvalWorkflow.approvers, policyID],
);
const approverTitle = useCallback(
(index: number) =>
diff --git a/src/components/FormHelpMessage.tsx b/src/components/FormHelpMessage.tsx
index 92cdc658b2d7..ab74a7d74b94 100644
--- a/src/components/FormHelpMessage.tsx
+++ b/src/components/FormHelpMessage.tsx
@@ -1,11 +1,13 @@
import isEmpty from 'lodash/isEmpty';
-import React from 'react';
+import React, {useMemo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import Parser from '@libs/Parser';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
+import RenderHTML from './RenderHTML';
import Text from './Text';
type FormHelpMessageProps = {
@@ -23,11 +25,29 @@ type FormHelpMessageProps = {
/** Whether to show dot indicator */
shouldShowRedDotIndicator?: boolean;
+
+ /** Whether should render error text as HTML or as Text */
+ shouldRenderMessageAsHTML?: boolean;
};
-function FormHelpMessage({message = '', children, isError = true, style, shouldShowRedDotIndicator = true}: FormHelpMessageProps) {
+function FormHelpMessage({message = '', children, isError = true, style, shouldShowRedDotIndicator = true, shouldRenderMessageAsHTML = false}: FormHelpMessageProps) {
const theme = useTheme();
const styles = useThemeStyles();
+
+ const HTMLMessage = useMemo(() => {
+ if (typeof message !== 'string' || !shouldRenderMessageAsHTML) {
+ return '';
+ }
+
+ const replacedText = Parser.replace(message, {shouldEscapeText: false});
+
+ if (isError) {
+ return `${replacedText}`;
+ }
+
+ return `${replacedText}`;
+ }, [isError, message, shouldRenderMessageAsHTML]);
+
if (isEmpty(message) && isEmpty(children)) {
return null;
}
@@ -41,7 +61,7 @@ function FormHelpMessage({message = '', children, isError = true, style, shouldS
/>
)}
- {children ?? {message}}
+ {children ?? (shouldRenderMessageAsHTML ? : {message})}
);
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 91dac789d7cf..6757d0602691 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -261,6 +261,12 @@ type MenuItemBaseProps = {
/** Whether should render helper text as HTML or as Text */
shouldParseHelperText?: boolean;
+ /** Whether should render hint text as HTML or as Text */
+ shouldRenderHintAsHTML?: boolean;
+
+ /** Whether should render error text as HTML or as Text */
+ shouldRenderErrorAsHTML?: boolean;
+
/** Should check anonymous user in onPress function */
shouldCheckActionAllowedOnPress?: boolean;
@@ -394,6 +400,8 @@ function MenuItem(
shouldBlockSelection = false,
shouldParseTitle = false,
shouldParseHelperText = false,
+ shouldRenderHintAsHTML = false,
+ shouldRenderErrorAsHTML = false,
shouldCheckActionAllowedOnPress = true,
onSecondaryInteraction,
titleWithTooltips,
@@ -802,6 +810,7 @@ function MenuItem(
shouldShowRedDotIndicator={!!shouldShowRedDotIndicator}
message={errorText}
style={[styles.menuItemError, errorTextStyle]}
+ shouldRenderMessageAsHTML={shouldRenderErrorAsHTML}
/>
)}
{!!hintText && (
@@ -810,6 +819,7 @@ function MenuItem(
shouldShowRedDotIndicator={false}
message={hintText}
style={styles.menuItemError}
+ shouldRenderMessageAsHTML={shouldRenderHintAsHTML}
/>
)}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1d6ef0a00c89..95fc367504b4 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7,6 +7,7 @@ import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
+ ApprovalWorkflowErrorParams,
ApprovedAmountParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
@@ -1310,6 +1311,10 @@ export default {
/* eslint-enable @typescript-eslint/naming-convention */
},
},
+ approverInMultipleWorkflows: ({name1, name2}: ApprovalWorkflowErrorParams) =>
+ `${name1} already approves reports to ${name2} in a separate workflow. If you change this approval relationship, all other workflows will be updated.`,
+ approverCircularReference: ({name1, name2}: ApprovalWorkflowErrorParams) =>
+ `${name1} already approves reports to ${name2}. Please choose a different approver to avoid a circular workflow.`,
},
workflowsDelayedSubmissionPage: {
autoReportingErrorMessage: "Delayed submission couldn't be changed. Please try again or contact support.",
@@ -1317,7 +1322,10 @@ export default {
monthlyOffsetErrorMessage: "Monthly frequency couldn't be changed. Please try again or contact support.",
},
workflowsCreateApprovalsPage: {
- title: 'Add approval workflow',
+ title: 'Confirm',
+ header: 'Add more approvers and confirm.',
+ additionalApprover: 'Additional approver',
+ submitButton: 'Add workflow',
},
workflowsEditApprovalsPage: {
title: 'Edit approval workflow',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 55f2dca91fe6..4658c8307a45 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -5,6 +5,7 @@ import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
+ ApprovalWorkflowErrorParams,
ApprovedAmountParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
@@ -1319,6 +1320,10 @@ export default {
/* eslint-enable @typescript-eslint/naming-convention */
},
},
+ approverInMultipleWorkflows: ({name1, name2}: ApprovalWorkflowErrorParams) =>
+ `${name1} ya aprueba informes a ${name2} en otro flujo de trabajo Si modificas esta relación de aprobación, se actualizarán todos los demás flujos de trabajo.`,
+ approverCircularReference: ({name1, name2}: ApprovalWorkflowErrorParams) =>
+ `${name1} ya aprueba informes a ${name2}. Por favor, elige un aprobador diferente para evitar un flujo de trabajo circular.`,
},
workflowsDelayedSubmissionPage: {
autoReportingErrorMessage: 'El parámetro de envío retrasado no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.',
@@ -1326,7 +1331,10 @@ export default {
monthlyOffsetErrorMessage: 'La frecuencia mensual no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.',
},
workflowsCreateApprovalsPage: {
- title: 'Añadir flujo de aprobación',
+ title: 'Confirmar',
+ header: 'Agrega más aprobadores y confirma.',
+ additionalApprover: 'Añadir aprobador',
+ submitButton: 'Añadir flujo de trabajo',
},
workflowsEditApprovalsPage: {
title: 'Edicion flujo de aprobación',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index b2e80ae3e973..f3d6f5b677e6 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -353,6 +353,11 @@ type IssueVirtualCardParams = {
link: string;
};
+type ApprovalWorkflowErrorParams = {
+ name1: string;
+ name2: string;
+};
+
export type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -475,4 +480,5 @@ export type {
UnapprovedParams,
RemoveMembersWarningPrompt,
DeleteExpenseTranslationParams,
+ ApprovalWorkflowErrorParams,
};
diff --git a/src/libs/API/parameters/WorkspaceApprovalParams.ts b/src/libs/API/parameters/WorkspaceApprovalParams.ts
index 67f96b7852e7..703d4976c045 100644
--- a/src/libs/API/parameters/WorkspaceApprovalParams.ts
+++ b/src/libs/API/parameters/WorkspaceApprovalParams.ts
@@ -1,9 +1,15 @@
-import type {PolicyEmployee} from '@src/types/onyx';
-
type CreateWorkspaceApprovalParams = {
authToken: string;
policyID: string;
- employees: PolicyEmployee[];
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array<{
+ * email: string;
+ * forwardsTo?: string;
+ * submitsTo?: string;
+ * }>
+ */
+ employees: string;
};
type UpdateWorkspaceApprovalParams = CreateWorkspaceApprovalParams;
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 25a12c47eadd..bffd7e610741 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1123,6 +1123,7 @@ type FullScreenNavigatorParamList = {
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: {
policyID: string;
@@ -1130,10 +1131,12 @@ type FullScreenNavigatorParamList = {
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EXPENSES_FROM]: {
policyID: string;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVER]: {
policyID: string;
approverIndex?: number;
+ backTo?: Routes;
};
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: {
policyID: string;
diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts
index 1365de4f881b..71348368e0d5 100644
--- a/src/libs/WorkflowUtils.ts
+++ b/src/libs/WorkflowUtils.ts
@@ -1,14 +1,17 @@
import lodashMapKeys from 'lodash/mapKeys';
-import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow';
+import type {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+import type {ApprovalWorkflowOnyx, Approver, Member} from '@src/types/onyx/ApprovalWorkflow';
import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
import type {PersonalDetailsList} from '@src/types/onyx/PersonalDetails';
import type {PolicyEmployeeList} from '@src/types/onyx/PolicyEmployee';
-const EMPTY_APPROVAL_WORKFLOW: ApprovalWorkflow = {
+const INITIAL_APPROVAL_WORKFLOW: ApprovalWorkflowOnyx = {
members: [],
approvers: [],
+ availableMembers: [],
isDefault: false,
- isBeingEdited: false,
+ action: CONST.APPROVAL_WORKFLOW.ACTION.CREATE,
isLoading: false,
};
@@ -29,8 +32,8 @@ type GetApproversParams = {
firstEmail: string;
};
-/** Get the list of approvers for a given workflow */
-function getApprovalWorkflowApprovers({employees, firstEmail, personalDetailsByEmail}: GetApproversParams): Approver[] {
+/** Get the list of approvers for a given email */
+function calculateApprovers({employees, firstEmail, personalDetailsByEmail}: GetApproversParams): Approver[] {
const approvers: Approver[] = [];
// Keep track of approver emails to detect circular references
const currentApproverEmails = new Set();
@@ -98,14 +101,13 @@ function convertPolicyEmployeesToApprovalWorkflows({employees, defaultApprover,
const member: Member = {email, avatar: personalDetailsByEmail[email]?.avatar, displayName: personalDetailsByEmail[email]?.displayName ?? email};
if (!approvalWorkflows[submitsTo]) {
- const approvers = getApprovalWorkflowApprovers({employees, firstEmail: submitsTo, personalDetailsByEmail});
+ const approvers = calculateApprovers({employees, firstEmail: submitsTo, personalDetailsByEmail});
approvers.forEach((approver) => (approverCountsByEmail[approver.email] = (approverCountsByEmail[approver.email] ?? 0) + 1));
approvalWorkflows[submitsTo] = {
members: [],
approvers,
isDefault: defaultApprover === submitsTo,
- isBeingEdited: false,
};
}
approvalWorkflows[submitsTo].members.push(member);
@@ -117,9 +119,23 @@ function convertPolicyEmployeesToApprovalWorkflows({employees, defaultApprover,
return -1;
}
+ if (b.isDefault) {
+ return 1;
+ }
+
return (a.approvers[0]?.displayName ?? '-1').localeCompare(b.approvers[0]?.displayName ?? '-1');
});
+ // Add a default workflow if one doesn't exist (no employees submit to the default approver)
+ const firstWorkflow = sortedApprovalWorkflows.at(0);
+ if (firstWorkflow && !firstWorkflow.isDefault) {
+ sortedApprovalWorkflows.unshift({
+ members: [],
+ approvers: calculateApprovers({employees, firstEmail: defaultApprover, personalDetailsByEmail}),
+ isDefault: true,
+ });
+ }
+
// Add a flag to each approver to indicate if they are in multiple workflows
return sortedApprovalWorkflows.map((workflow) => ({
...workflow,
@@ -142,13 +158,13 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
employeeList: PolicyEmployeeList;
/**
- * Should the workflow be removed from the employees
+ * Mode to use when converting the approval workflow
*/
- removeWorkflow?: boolean;
+ type: ValueOf;
};
/** Convert an approval workflow to a list of policy employees */
-function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, removeWorkflow = false}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
+function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
const updatedEmployeeList: PolicyEmployeeList = {};
const firstApprover = approvalWorkflow.approvers.at(0);
@@ -157,25 +173,21 @@ function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeLis
}
approvalWorkflow.approvers.forEach((approver, index) => {
- if (updatedEmployeeList[approver.email]) {
- return;
- }
-
const nextApprover = approvalWorkflow.approvers.at(index + 1);
updatedEmployeeList[approver.email] = {
...employeeList[approver.email],
- forwardsTo: removeWorkflow ? undefined : nextApprover?.email,
+ forwardsTo: type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : nextApprover?.email ?? '',
};
});
approvalWorkflow.members.forEach(({email}) => {
updatedEmployeeList[email] = {
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : employeeList[email]),
- submitsTo: removeWorkflow ? undefined : firstApprover.email,
+ submitsTo: type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : firstApprover.email ?? '',
};
});
return updatedEmployeeList;
}
-export {getApprovalWorkflowApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, EMPTY_APPROVAL_WORKFLOW};
+export {calculateApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, INITIAL_APPROVAL_WORKFLOW};
diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts
index 77607080bc7a..23158308ddd0 100644
--- a/src/libs/actions/Workflow.ts
+++ b/src/libs/actions/Workflow.ts
@@ -1,15 +1,19 @@
+import lodashDropRightWhile from 'lodash/dropRightWhile';
+import lodashMapKeys from 'lodash/mapKeys';
import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import type {CreateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams, UpdateWorkspaceApprovalParams} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
-import {convertApprovalWorkflowToPolicyEmployees} from '@libs/WorkflowUtils';
+import {calculateApprovers, convertApprovalWorkflowToPolicyEmployees} from '@libs/WorkflowUtils';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ApprovalWorkflow, Policy} from '@src/types/onyx';
+import type {ApprovalWorkflowOnyx, PersonalDetailsList, Policy} from '@src/types/onyx';
import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow';
+import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
-let currentApprovalWorkflow: ApprovalWorkflow | undefined;
+let currentApprovalWorkflow: ApprovalWorkflowOnyx | undefined;
Onyx.connect({
key: ONYXKEYS.APPROVAL_WORKFLOW,
callback: (approvalWorkflow) => {
@@ -32,8 +36,16 @@ Onyx.connect({
},
});
+let personalDetails: PersonalDetailsList | undefined;
+Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ callback: (value) => {
+ personalDetails = value;
+ },
+});
+
function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow) {
- const policy = allPolicies?.[policyID];
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
if (!authToken || !policy) {
return;
@@ -41,7 +53,7 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
const previousEmployeeList = {...policy.employeeList};
const previousApprovalMode = policy.approvalMode;
- const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList});
+ const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, type: CONST.APPROVAL_WORKFLOW.TYPE.CREATE});
const optimisticData: OnyxUpdate[] = [
{
@@ -57,6 +69,7 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
value: {
employeeList: updatedEmployees,
approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED,
+ pendingFields: {employeeList: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
},
},
];
@@ -73,6 +86,7 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
value: {
employeeList: previousEmployeeList,
approvalMode: previousApprovalMode,
+ pendingFields: {employeeList: null},
},
},
];
@@ -83,21 +97,28 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
key: ONYXKEYS.APPROVAL_WORKFLOW,
value: null,
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {employeeList: null},
+ },
+ },
];
- const parameters: CreateWorkspaceApprovalParams = {policyID, authToken, employees: Object.values(updatedEmployees)};
+ const parameters: CreateWorkspaceApprovalParams = {policyID, authToken, employees: JSON.stringify(Object.values(updatedEmployees))};
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}
function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow) {
- const policy = allPolicies?.[policyID];
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
if (!authToken || !policy) {
return;
}
const previousEmployeeList = {...policy.employeeList};
- const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList});
+ const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, type: CONST.APPROVAL_WORKFLOW.TYPE.UPDATE});
const optimisticData: OnyxUpdate[] = [
{
@@ -110,7 +131,10 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- value: {employeeList: updatedEmployees},
+ value: {
+ employeeList: updatedEmployees,
+ pendingFields: {employeeList: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
},
];
@@ -123,7 +147,10 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- value: {employeeList: previousEmployeeList},
+ value: {
+ employeeList: previousEmployeeList,
+ pendingFields: {employeeList: null},
+ },
},
];
@@ -133,21 +160,28 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
key: ONYXKEYS.APPROVAL_WORKFLOW,
value: null,
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {employeeList: null},
+ },
+ },
];
- const parameters: UpdateWorkspaceApprovalParams = {policyID, authToken, employees: Object.values(updatedEmployees)};
+ const parameters: UpdateWorkspaceApprovalParams = {policyID, authToken, employees: JSON.stringify(Object.values(updatedEmployees))};
API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}
function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow) {
- const policy = allPolicies?.[policyID];
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
if (!authToken || !policy) {
return;
}
const previousEmployeeList = {...policy.employeeList};
- const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, removeWorkflow: true});
+ const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE});
const updatedEmployeeList = {...previousEmployeeList, ...updatedEmployees};
// If there is more than one workflow, we need to keep the advanced approval mode (first workflow is the default)
@@ -167,6 +201,7 @@ function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
value: {
employeeList: updatedEmployees,
approvalMode: hasMoreThanOneWorkflow ? CONST.POLICY.APPROVAL_MODE.ADVANCED : CONST.POLICY.APPROVAL_MODE.BASIC,
+ pendingFields: {employeeList: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
},
},
];
@@ -185,6 +220,13 @@ function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED,
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {employeeList: null},
+ },
+ },
];
const successData: OnyxUpdate[] = [
@@ -193,38 +235,120 @@ function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
key: ONYXKEYS.APPROVAL_WORKFLOW,
value: null,
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {employeeList: null},
+ },
+ },
];
- const parameters: RemoveWorkspaceApprovalParams = {policyID, authToken, employees: Object.values(updatedEmployees)};
+ const parameters: RemoveWorkspaceApprovalParams = {policyID, authToken, employees: JSON.stringify(Object.values(updatedEmployees))};
API.write(WRITE_COMMANDS.REMOVE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}
function setApprovalWorkflowMembers(members: Member[]) {
- Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {members});
+ Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {members, errors: null});
}
-function setApprovalWorkflowApprover(approver: Approver, index: number) {
- if (!currentApprovalWorkflow) {
+/**
+ * Set the approver at the specified index in the current approval workflow
+ * @param approver - The new approver to set
+ * @param approverIndex - The index of the approver to set
+ * @param policyID - The ID of the policy
+ */
+function setApprovalWorkflowApprover(approver: Approver, approverIndex: number, policyID: string) {
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+
+ if (!currentApprovalWorkflow || !policy?.employeeList) {
return;
}
- const updatedApprovers = [...currentApprovalWorkflow.approvers];
- updatedApprovers[index] = approver;
- Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: updatedApprovers});
+ const approvers: Array = [...currentApprovalWorkflow.approvers];
+ approvers[approverIndex] = approver;
+
+ // Check if the approver forwards to other approvers and add them to the list
+ if (policy.employeeList[approver.email]?.forwardsTo) {
+ const personalDetailsByEmail = lodashMapKeys(personalDetails, (value, key) => value?.login ?? key);
+ const additionalApprovers = calculateApprovers({employees: policy.employeeList, firstEmail: approver.email, personalDetailsByEmail}).map((additionalApprover) => ({
+ ...additionalApprover,
+ isInMultipleWorkflows: true,
+ }));
+ approvers.splice(approverIndex, approvers.length, ...additionalApprovers);
+ }
+
+ const errors: Record = {additionalApprover: null};
+ // Check for circular references and reset errors
+ const updatedApprovers = approvers.map((existingApprover, index) => {
+ if (!existingApprover) {
+ return;
+ }
+
+ const hasCircularReference = approvers.slice(0, index).some((previousApprover) => existingApprover.email === previousApprover?.email);
+ if (hasCircularReference) {
+ errors[`approver-${index}`] = 'workflowsPage.approverCircularReference';
+ } else {
+ errors[`approver-${index}`] = null;
+ }
+
+ return {
+ ...existingApprover,
+ isCircularReference: hasCircularReference,
+ };
+ });
+
+ Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: updatedApprovers, errors});
}
-function setApprovalWorkflow(approvalWorkflow: ApprovalWorkflow) {
- Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
+function clearApprovalWorkflowApprover(approverIndex: number) {
+ if (!currentApprovalWorkflow) {
+ return;
+ }
+
+ const approvers: Array = [...currentApprovalWorkflow.approvers];
+ approvers[approverIndex] = undefined;
+
+ Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: lodashDropRightWhile(approvers, (approver) => !approver), errors: null});
}
function clearApprovalWorkflowApprovers() {
Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: []});
}
+function setApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx) {
+ Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, approvalWorkflow);
+}
+
function clearApprovalWorkflow() {
Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, null);
}
+function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): Record {
+ const errors: Record = {};
+
+ approvalWorkflow.approvers.forEach((approver, approverIndex) => {
+ if (!approver) {
+ errors[`approver-${approverIndex}`] = 'common.error.fieldRequired';
+ }
+
+ if (approver?.isCircularReference) {
+ errors[`approver-${approverIndex}`] = 'workflowsPage.approverCircularReference';
+ }
+ });
+
+ if (!approvalWorkflow.members.length) {
+ errors.members = 'common.error.fieldRequired';
+ }
+
+ if (!approvalWorkflow.approvers.length) {
+ errors.additionalApprover = 'common.error.fieldRequired';
+ }
+
+ Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {errors});
+ return errors;
+}
+
export {
createApprovalWorkflow,
updateApprovalWorkflow,
@@ -232,6 +356,8 @@ export {
setApprovalWorkflowMembers,
setApprovalWorkflowApprover,
setApprovalWorkflow,
+ clearApprovalWorkflowApprover,
clearApprovalWorkflowApprovers,
clearApprovalWorkflow,
+ validateApprovalWorkflow,
};
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index b513ebccc3b3..4460e1c32961 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -26,7 +26,7 @@ import {getPaymentMethodDescription} from '@libs/PaymentUtils';
import Permissions from '@libs/Permissions';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
-import {convertPolicyEmployeesToApprovalWorkflows, EMPTY_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils';
+import {convertPolicyEmployeesToApprovalWorkflows, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils';
import type {FullScreenNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
@@ -63,19 +63,24 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const policyApproverName = useMemo(() => PersonalDetailsUtils.getPersonalDetailByEmail(policyApproverEmail ?? '')?.displayName ?? policyApproverEmail, [policyApproverEmail]);
const approvalWorkflows = useMemo(
- () => convertPolicyEmployeesToApprovalWorkflows({employees: policy?.employeeList ?? {}, defaultApprover: policyApproverEmail ?? '', personalDetails: personalDetails ?? {}}),
- [personalDetails, policy?.employeeList, policyApproverEmail],
+ () =>
+ convertPolicyEmployeesToApprovalWorkflows({
+ employees: policy?.employeeList ?? {},
+ defaultApprover: policyApproverEmail ?? policy?.owner ?? '',
+ personalDetails: personalDetails ?? {},
+ }),
+ [personalDetails, policy?.employeeList, policy?.owner, policyApproverEmail],
);
const displayNameForAuthorizedPayer = useMemo(
() => PersonalDetailsUtils.getPersonalDetailByEmail(policy?.achAccount?.reimburser ?? '')?.displayName ?? policy?.achAccount?.reimburser,
[policy?.achAccount?.reimburser],
);
- const onPressAutoReportingFrequency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.getRoute(policy?.id ?? '')), [policy?.id]);
+ const onPressAutoReportingFrequency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.getRoute(route.params.policyID)), [route.params.policyID]);
const fetchData = useCallback(() => {
- Policy.openPolicyWorkflowsPage(policy?.id ?? route.params.policyID);
- }, [policy?.id, route.params.policyID]);
+ Policy.openPolicyWorkflowsPage(route.params.policyID);
+ }, [route.params.policyID]);
const confirmCurrencyChangeAndHideModal = useCallback(() => {
if (!policy) {
@@ -104,7 +109,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
return;
}
Workflow.setApprovalWorkflow({
- ...EMPTY_APPROVAL_WORKFLOW,
+ ...INITIAL_APPROVAL_WORKFLOW,
availableMembers: approvalWorkflows.at(0)?.members ?? [],
});
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(route.params.policyID));
@@ -154,7 +159,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
isActive: (policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT && !hasDelayedSubmissionError) ?? false,
pendingAction: policy?.pendingFields?.autoReporting,
errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING),
- onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING),
+ onCloseError: () => Policy.clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING),
},
{
title: translate('workflowsPage.addApprovalsTitle'),
@@ -165,11 +170,17 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
},
subMenuItems: canUseAdvancedApproval ? (
<>
- {approvalWorkflows.map((w) => (
-
+ {approvalWorkflows.map((workflow, index) => (
+
+
+
))}
),
- isActive: (policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC && !hasApprovalError) ?? false,
+ isActive:
+ ([CONST.POLICY.APPROVAL_MODE.BASIC, CONST.POLICY.APPROVAL_MODE.ADVANCED].some((approvalMode) => approvalMode === policy?.approvalMode) && !hasApprovalError) ?? false,
pendingAction: policy?.pendingFields?.approvalMode,
errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.APPROVAL_MODE),
- onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.COLLECTION_KEYS.APPROVAL_MODE),
+ onCloseError: () => Policy.clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.APPROVAL_MODE),
},
{
title: translate('workflowsPage.makeOrTrackPaymentsTitle'),
@@ -214,7 +226,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
}
const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner;
- Policy.setWorkspaceReimbursement(policy?.id ?? '-1', newReimbursementChoice, newReimburserEmail ?? '');
+ Policy.setWorkspaceReimbursement(route.params.policyID, newReimbursementChoice, newReimburserEmail ?? '');
},
subMenuItems:
!isOffline && policy?.isLoadingWorkspaceReimbursement === true ? (
@@ -278,7 +290,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
isActive: policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO,
pendingAction: policy?.pendingFields?.reimbursementChoice,
errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE),
- onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE),
+ onCloseError: () => Policy.clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE),
},
];
}, [
diff --git a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx
new file mode 100644
index 000000000000..5c060a0e2fba
--- /dev/null
+++ b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx
@@ -0,0 +1,136 @@
+import type {ForwardedRef} from 'react';
+import React, {forwardRef, useCallback} from 'react';
+import {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as ScrollViewRN} from 'react-native';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type {ApprovalWorkflowOnyx} from '@src/types/onyx';
+import type {Approver} from '@src/types/onyx/ApprovalWorkflow';
+
+type ApprovalWorkflowEditorProps = {
+ approvalWorkflow: ApprovalWorkflowOnyx;
+ policyID: string;
+};
+
+function ApprovalWorkflowEditor({approvalWorkflow, policyID}: ApprovalWorkflowEditorProps, ref: ForwardedRef) {
+ const styles = useThemeStyles();
+ const {translate, toLocaleOrdinal} = useLocalize();
+
+ const approverDescription = useCallback(
+ (index: number) =>
+ approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`,
+ [approvalWorkflow.approvers.length, toLocaleOrdinal, translate],
+ );
+ const members = approvalWorkflow.isDefault ? translate('workspace.common.everyone') : approvalWorkflow.members.map((m) => m.displayName).join(', ');
+
+ const approverErrorMessage = useCallback(
+ (approver: Approver | undefined, approverIndex: number) => {
+ const previousApprover = approvalWorkflow.approvers.slice(0, approverIndex).filter(Boolean).at(-1);
+ const error = approvalWorkflow?.errors?.[`approver-${approverIndex}`];
+
+ if (!error) {
+ return;
+ }
+
+ if (error === 'workflowsPage.approverCircularReference') {
+ if (!previousApprover || !approver) {
+ return;
+ }
+ return translate('workflowsPage.approverCircularReference', {
+ name1: approver.displayName,
+ name2: previousApprover.displayName,
+ });
+ }
+
+ return translate(error);
+ },
+ [approvalWorkflow.approvers, approvalWorkflow.errors, translate],
+ );
+
+ const approverHintMessage = useCallback(
+ (approver: Approver | undefined, approverIndex: number) => {
+ const previousApprover = approvalWorkflow.approvers.slice(0, approverIndex).filter(Boolean).at(-1);
+ if (approver?.isInMultipleWorkflows && approver.email === previousApprover?.forwardsTo) {
+ return translate('workflowsPage.approverInMultipleWorkflows', {
+ name1: approver.displayName,
+ name2: previousApprover.displayName,
+ });
+ }
+ },
+ [approvalWorkflow.approvers, translate],
+ );
+
+ return (
+
+
+ {translate('workflowsCreateApprovalsPage.header')}
+
+ Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(policyID, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)))}
+ shouldShowRightIcon
+ wrapperStyle={[styles.sectionMenuItemTopDescription]}
+ errorText={approvalWorkflow?.errors?.members ? translate(approvalWorkflow.errors.members) : undefined}
+ brickRoadIndicator={approvalWorkflow?.errors?.members ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+ {approvalWorkflow.approvers.map((approver, approverIndex) => {
+ const errorText = approverErrorMessage(approver, approverIndex);
+ const hintText = !errorText && approverHintMessage(approver, approverIndex);
+ return (
+
+ Navigation.navigate(
+ ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverIndex, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)),
+ )
+ }
+ shouldShowRightIcon
+ hintText={hintText}
+ shouldRenderHintAsHTML
+ brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ errorText={errorText}
+ shouldRenderErrorAsHTML
+ />
+ );
+ })}
+
+
+ Navigation.navigate(
+ ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approvalWorkflow.approvers.length, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)),
+ )
+ }
+ shouldShowRightIcon
+ wrapperStyle={styles.sectionMenuItemTopDescription}
+ errorText={approvalWorkflow?.errors?.additionalApprover ? translate(approvalWorkflow.errors.additionalApprover) : undefined}
+ brickRoadIndicator={approvalWorkflow?.errors?.additionalApprover ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+
+ );
+}
+
+ApprovalWorkflowEditor.displayName = 'ApprovalWorkflowEditor';
+
+export default forwardRef(ApprovalWorkflowEditor);
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
index 0879940f7247..fc9274db4b06 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
@@ -81,7 +81,9 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy);
- const approverIndex = route.params.approverIndex ?? 0;
+ const approverIndex = Number(route.params.approverIndex) ?? 0;
+ const isInitialCreationFlow = approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && !route.params.backTo;
+ const defaultApprover = policy?.approver ?? policy?.owner;
useEffect(() => {
const currentApprover = approvalWorkflow?.approvers[approverIndex];
@@ -105,6 +107,17 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
return null;
}
+ // Do not allow the same email to be added twice
+ const isEmailAlreadyInApprovers = approvalWorkflow?.approvers.some((approver, index) => approver?.email === email && index !== approverIndex);
+ if (isEmailAlreadyInApprovers && selectedApproverEmail !== email) {
+ return null;
+ }
+
+ // Do not allow the default approver to be added as the first approver
+ if (!approvalWorkflow?.isDefault && approverIndex === 0 && defaultApprover === email) {
+ return null;
+ }
+
const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
const {avatar, displayName = email} = personalDetails?.[accountID] ?? {};
@@ -140,54 +153,64 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
shouldShow: true,
},
];
- }, [debouncedSearchTerm, personalDetails, policy?.employeeList, selectedApproverEmail, translate]);
+ }, [
+ approvalWorkflow?.approvers,
+ approvalWorkflow?.isDefault,
+ approverIndex,
+ debouncedSearchTerm,
+ defaultApprover,
+ personalDetails,
+ policy?.employeeList,
+ selectedApproverEmail,
+ translate,
+ ]);
const nextStep = useCallback(() => {
- if (!selectedApproverEmail) {
- return;
+ if (selectedApproverEmail) {
+ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
+ const accountID = Number(policyMemberEmailsToAccountIDs[selectedApproverEmail] ?? '');
+ const {avatar, displayName = selectedApproverEmail} = personalDetails?.[accountID] ?? {};
+ Workflow.setApprovalWorkflowApprover(
+ {
+ email: selectedApproverEmail,
+ avatar,
+ displayName,
+ },
+ approverIndex,
+ route.params.policyID,
+ );
+ } else {
+ Workflow.clearApprovalWorkflowApprover(approverIndex);
}
- const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
- const accountID = Number(policyMemberEmailsToAccountIDs[selectedApproverEmail] ?? '');
- const {avatar, displayName = selectedApproverEmail} = personalDetails?.[accountID] ?? {};
- Workflow.setApprovalWorkflowApprover(
- {
- email: selectedApproverEmail,
- avatar,
- displayName,
- isInMultipleWorkflows: false,
- isCircularReference: false,
- },
- approverIndex,
- );
-
- const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? '';
- if (!approvalWorkflow?.isBeingEdited && firstApprover) {
- Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover));
+ if (approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE) {
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID), CONST.NAVIGATION.TYPE.UP);
} else {
- Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID));
+ const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? '';
+ Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover));
}
- }, [approvalWorkflow?.approvers, approvalWorkflow?.isBeingEdited, approverIndex, personalDetails, policy?.employeeList, route.params.policyID, selectedApproverEmail]);
+ }, [approvalWorkflow, approverIndex, personalDetails, policy?.employeeList, route.params.policyID, selectedApproverEmail]);
const nextButton = useMemo(
() => (
),
- [nextStep, selectedApproverEmail, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate],
+ [isInitialCreationFlow, nextStep, selectedApproverEmail, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate],
);
const goBack = useCallback(() => {
- if (!approvalWorkflow?.isBeingEdited) {
+ if (isInitialCreationFlow) {
Workflow.clearApprovalWorkflowApprovers();
}
Navigation.goBack();
- }, [approvalWorkflow?.isBeingEdited]);
+ }, [isInitialCreationFlow]);
const toggleApprover = (approver: SelectionListApprover) => {
if (selectedApproverEmail === approver.login) {
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
index d7a4696446cc..ecb9de748483 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
@@ -1,27 +1,54 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React from 'react';
+import React, {useCallback, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
import * as PolicyUtils from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Workflow from '@userActions/Workflow';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
+import type {Approver} from '@src/types/onyx/ApprovalWorkflow';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import ApprovalWorkflowEditor from './ApprovalWorkflowEditor';
type WorkspaceWorkflowsApprovalsCreatePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsCreatePageProps) {
+ const styles = useThemeStyles();
const {translate} = useLocalize();
+ const [approvalWorkflow, approvalWorkflowMetadata] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW);
+ const formRef = useRef(null);
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy);
+ const createApprovalWorkflow = useCallback(() => {
+ if (!approvalWorkflow) {
+ return;
+ }
+
+ if (!isEmptyObject(Workflow.validateApprovalWorkflow(approvalWorkflow))) {
+ return;
+ }
+
+ Workflow.createApprovalWorkflow(route.params.policyID, {...approvalWorkflow, approvers: approvalWorkflow.approvers as Approver[]});
+ Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID));
+ }, [approvalWorkflow, route.params.policyID]);
+
return (
+ {approvalWorkflowMetadata.status === 'loading' && }
+ {approvalWorkflow && (
+
+ )}
+
+ {
+ formRef.current?.scrollTo({y: 0, animated: true});
+ }}
+ isLoading={approvalWorkflow?.isLoading}
+ buttonText={translate('workflowsCreateApprovalsPage.submitButton')}
+ containerStyles={[styles.mb5, styles.mh5]}
+ enabledWhenOffline
+ />
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
index 4a504275bbb2..9b1e0d7f3c23 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
@@ -57,6 +57,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy);
+ const isInitialCreationFlow = approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && !route.params.backTo;
useEffect(() => {
if (!approvalWorkflow?.members) {
@@ -129,32 +130,37 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
const members: Member[] = selectedMembers.map((member) => ({displayName: member.text, avatar: member.icons?.[0]?.source, email: member.login}));
Workflow.setApprovalWorkflowMembers(members);
- const firstApprover = approvalWorkflow?.approvers?.[0]?.email;
- if (approvalWorkflow?.isBeingEdited && firstApprover) {
- Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover));
- } else {
+ if (route.params.backTo) {
+ Navigation.navigate(route.params.backTo);
+ return;
+ }
+
+ if (approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE) {
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(route.params.policyID, 0));
+ } else {
+ const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? '';
+ Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover));
}
- }, [approvalWorkflow?.approvers, approvalWorkflow?.isBeingEdited, route.params.policyID, selectedMembers]);
+ }, [approvalWorkflow, route.params.backTo, route.params.policyID, selectedMembers]);
const goBack = useCallback(() => {
- if (!approvalWorkflow?.isBeingEdited) {
+ if (isInitialCreationFlow) {
Workflow.clearApprovalWorkflow();
}
Navigation.goBack();
- }, [approvalWorkflow?.isBeingEdited]);
+ }, [isInitialCreationFlow]);
const nextButton = useMemo(
() => (
),
- [nextStep, selectedMembers.length, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate],
+ [isInitialCreationFlow, nextStep, selectedMembers.length, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate],
);
const toggleMember = (member: SelectionListMember) => {
diff --git a/src/types/onyx/ApprovalWorkflow.ts b/src/types/onyx/ApprovalWorkflow.ts
index b5bce8037fdc..44a8932341fc 100644
--- a/src/types/onyx/ApprovalWorkflow.ts
+++ b/src/types/onyx/ApprovalWorkflow.ts
@@ -1,4 +1,7 @@
+import type {ValueOf} from 'type-fest';
import type {AvatarSource} from '@libs/UserUtils';
+import type CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
/**
* Approver in the approval workflow
@@ -78,22 +81,39 @@ type ApprovalWorkflow = {
* Is this the default workflow for the policy (first approver of this workflow is the same as the policy's default approver)
*/
isDefault: boolean;
+};
+
+/**
+ * Approval workflow for a group of employees with additional properties for the Onyx store
+ */
+type ApprovalWorkflowOnyx = Omit & {
+ /**
+ * List of approvers in the workflow (the order of approvers in this array is important)
+ *
+ * The first approver in the array is the first approver in the workflow, next approver is the one they forward to, etc.
+ */
+ approvers: Array;
/**
- * Is this workflow being edited vs created
+ * The current action of the workflow, used to navigate between different screens
*/
- isBeingEdited: boolean;
+ action: ValueOf;
/**
* Whether we are waiting for the API action to complete
*/
- isLoading?: boolean;
+ isLoading: boolean;
/**
* List of available members that can be selected in the workflow
*/
- availableMembers?: Member[];
+ availableMembers: Member[];
+
+ /**
+ * Errors for the workflow
+ */
+ errors?: Record;
};
export default ApprovalWorkflow;
-export type {Approver, Member};
+export type {ApprovalWorkflowOnyx, Approver, Member};
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 3311959cf8bf..8f48205d8749 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -1555,7 +1555,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Workspace account ID configured for Expensify Card */
workspaceAccountID?: number;
} & Partial,
- 'generalSettings' | 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes
+ 'generalSettings' | 'addWorkspaceRoom' | 'employeeList' | keyof ACHAccount | keyof Attributes
>;
/** Stages of policy connection sync */
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 8c800f32bf1c..2bb129708981 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -1,6 +1,6 @@
import type Account from './Account';
import type AccountData from './AccountData';
-import type ApprovalWorkflow from './ApprovalWorkflow';
+import type {ApprovalWorkflowOnyx} from './ApprovalWorkflow';
import type {BankAccountList} from './BankAccount';
import type BankAccount from './BankAccount';
import type Beta from './Beta';
@@ -217,7 +217,7 @@ export type {
StripeCustomerID,
BillingStatus,
CancellationDetails,
- ApprovalWorkflow,
+ ApprovalWorkflowOnyx,
MobileSelectionMode,
WorkspaceTooltip,
};
diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts
index 6359ba01ed23..81b1e559e5f6 100644
--- a/tests/unit/WorkflowUtilsTest.ts
+++ b/tests/unit/WorkflowUtilsTest.ts
@@ -43,7 +43,6 @@ function buildWorkflow(memberIDs: number[], approverIDs: number[], workflow: Par
members: memberIDs.map(buildMember),
approvers: approverIDs.map((id) => buildApprover(id)),
isDefault: false,
- isBeingEdited: false,
...workflow,
};
}
@@ -57,11 +56,11 @@ describe('WorkflowUtils', () => {
}
});
- describe('getApprovalWorkflowApprovers', () => {
+ describe('calculateApprovers', () => {
it('Should return no approvers for empty employees object', () => {
const employees: PolicyEmployeeList = {};
const firstEmail = '1@example.com';
- const approvers = WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail, personalDetailsByEmail});
+ const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail, personalDetailsByEmail});
expect(approvers).toEqual([]);
});
@@ -78,7 +77,7 @@ describe('WorkflowUtils', () => {
},
};
const firstEmail = '1@example.com';
- const approvers = WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail, personalDetailsByEmail});
+ const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail, personalDetailsByEmail});
expect(approvers).toEqual([buildApprover(1)]);
});
@@ -95,7 +94,7 @@ describe('WorkflowUtils', () => {
},
};
const firstEmail = '1@example.com';
- const approvers = WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail, personalDetailsByEmail});
+ const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail, personalDetailsByEmail});
expect(approvers).toEqual([buildApprover(1)]);
});
@@ -124,18 +123,18 @@ describe('WorkflowUtils', () => {
},
};
- expect(WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([
+ expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([
buildApprover(1, {forwardsTo: '2@example.com'}),
buildApprover(2, {forwardsTo: '3@example.com'}),
buildApprover(3, {forwardsTo: '4@example.com'}),
buildApprover(4),
]);
- expect(WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([
+ expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([
buildApprover(2, {forwardsTo: '3@example.com'}),
buildApprover(3, {forwardsTo: '4@example.com'}),
buildApprover(4),
]);
- expect(WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail: '3@example.com', personalDetailsByEmail})).toEqual([
+ expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '3@example.com', personalDetailsByEmail})).toEqual([
buildApprover(3, {forwardsTo: '4@example.com'}),
buildApprover(4),
]);
@@ -165,7 +164,7 @@ describe('WorkflowUtils', () => {
},
};
- expect(WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([
+ expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([
buildApprover(1, {forwardsTo: '2@example.com'}),
buildApprover(2, {forwardsTo: '3@example.com'}),
buildApprover(3, {forwardsTo: '4@example.com'}),
@@ -173,7 +172,7 @@ describe('WorkflowUtils', () => {
buildApprover(5, {forwardsTo: '1@example.com'}),
buildApprover(1, {forwardsTo: '2@example.com', isCircularReference: true}),
]);
- expect(WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([
+ expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([
buildApprover(2, {forwardsTo: '3@example.com'}),
buildApprover(3, {forwardsTo: '4@example.com'}),
buildApprover(4, {forwardsTo: '5@example.com'}),
@@ -191,7 +190,7 @@ describe('WorkflowUtils', () => {
},
};
- expect(WorkflowUtils.getApprovalWorkflowApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([
+ expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([
buildApprover(1, {forwardsTo: '1@example.com'}),
buildApprover(1, {forwardsTo: '1@example.com', isCircularReference: true}),
]);
@@ -385,17 +384,16 @@ describe('WorkflowUtils', () => {
members: [buildMember(1), buildMember(2)],
approvers: [buildApprover(1)],
isDefault: true,
- isBeingEdited: false,
};
const employeeList: PolicyEmployeeList = {
'1@example.com': buildPolicyEmployee(1, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
'2@example.com': buildPolicyEmployee(2, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
};
- const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList});
+ const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type: 'create'});
expect(convertedEmployees).toEqual({
- '1@example.com': buildPolicyEmployee(1, {forwardsTo: undefined, submitsTo: '1@example.com', role: 'admin'}),
+ '1@example.com': buildPolicyEmployee(1, {forwardsTo: '', submitsTo: '1@example.com', role: 'admin'}),
'2@example.com': buildPolicyEmployee(2, {forwardsTo: 'previous@example.com', submitsTo: '1@example.com', role: 'admin'}),
});
});
@@ -405,7 +403,6 @@ describe('WorkflowUtils', () => {
members: [buildMember(4), buildMember(5), buildMember(6)],
approvers: [buildApprover(1, {forwardsTo: '2@example.com'}), buildApprover(2, {forwardsTo: '2@example.com'}), buildApprover(3)],
isDefault: false,
- isBeingEdited: false,
};
const employeeList: PolicyEmployeeList = {
'1@example.com': buildPolicyEmployee(1, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
@@ -416,53 +413,23 @@ describe('WorkflowUtils', () => {
'6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}),
};
- const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList});
+ const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type: 'create'});
expect(convertedEmployees).toEqual({
'1@example.com': buildPolicyEmployee(1, {forwardsTo: '2@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
'2@example.com': buildPolicyEmployee(2, {forwardsTo: '3@example.com', submitsTo: 'previous@example.com'}),
- '3@example.com': buildPolicyEmployee(3, {forwardsTo: undefined, submitsTo: 'previous@example.com'}),
+ '3@example.com': buildPolicyEmployee(3, {forwardsTo: '', submitsTo: 'previous@example.com'}),
'4@example.com': buildPolicyEmployee(4, {forwardsTo: 'previous@example.com', submitsTo: '1@example.com'}),
'5@example.com': buildPolicyEmployee(5, {forwardsTo: 'previous@example.com', submitsTo: '1@example.com'}),
'6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: '1@example.com'}),
});
});
- it('Should return an updated employee list for a workflow with a circular reference', () => {
- const approvalWorkflow: ApprovalWorkflow = {
- members: [buildMember(4)],
- approvers: [
- buildApprover(1, {forwardsTo: '2@example.com'}),
- buildApprover(2, {forwardsTo: '2@example.com'}),
- buildApprover(3, {forwardsTo: '1@example.com'}),
- buildApprover(1, {forwardsTo: '2@example.com'}),
- ],
- isDefault: false,
- isBeingEdited: false,
- };
- const employeeList: PolicyEmployeeList = {
- '1@example.com': buildPolicyEmployee(1, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
- '2@example.com': buildPolicyEmployee(2, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}),
- '3@example.com': buildPolicyEmployee(3, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}),
- '4@example.com': buildPolicyEmployee(4, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}),
- };
-
- const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList});
-
- expect(convertedEmployees).toEqual({
- '1@example.com': buildPolicyEmployee(1, {forwardsTo: '2@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
- '2@example.com': buildPolicyEmployee(2, {forwardsTo: '3@example.com', submitsTo: 'previous@example.com'}),
- '3@example.com': buildPolicyEmployee(3, {forwardsTo: '1@example.com', submitsTo: 'previous@example.com'}),
- '4@example.com': buildPolicyEmployee(4, {forwardsTo: 'previous@example.com', submitsTo: '1@example.com'}),
- });
- });
-
it('Should return an updated employee list for a complex workflow when removing', () => {
const approvalWorkflow: ApprovalWorkflow = {
members: [buildMember(4), buildMember(5), buildMember(6)],
approvers: [buildApprover(1, {forwardsTo: '2@example.com'}), buildApprover(2, {forwardsTo: '2@example.com'}), buildApprover(3)],
isDefault: false,
- isBeingEdited: false,
};
const employeeList: PolicyEmployeeList = {
'1@example.com': buildPolicyEmployee(1, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com', role: 'admin'}),
@@ -473,15 +440,15 @@ describe('WorkflowUtils', () => {
'6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}),
};
- const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, removeWorkflow: true});
+ const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type: 'remove'});
expect(convertedEmployees).toEqual({
- '1@example.com': buildPolicyEmployee(1, {forwardsTo: undefined, submitsTo: 'previous@example.com', role: 'admin'}),
- '2@example.com': buildPolicyEmployee(2, {forwardsTo: undefined, submitsTo: 'previous@example.com'}),
- '3@example.com': buildPolicyEmployee(3, {forwardsTo: undefined, submitsTo: 'previous@example.com'}),
- '4@example.com': buildPolicyEmployee(4, {forwardsTo: 'previous@example.com', submitsTo: undefined}),
- '5@example.com': buildPolicyEmployee(5, {forwardsTo: 'previous@example.com', submitsTo: undefined}),
- '6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: undefined}),
+ '1@example.com': buildPolicyEmployee(1, {forwardsTo: '', submitsTo: 'previous@example.com', role: 'admin'}),
+ '2@example.com': buildPolicyEmployee(2, {forwardsTo: '', submitsTo: 'previous@example.com'}),
+ '3@example.com': buildPolicyEmployee(3, {forwardsTo: '', submitsTo: 'previous@example.com'}),
+ '4@example.com': buildPolicyEmployee(4, {forwardsTo: 'previous@example.com', submitsTo: ''}),
+ '5@example.com': buildPolicyEmployee(5, {forwardsTo: 'previous@example.com', submitsTo: ''}),
+ '6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: ''}),
});
});
});