Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CRITICAL] Implement <WorkspaceWorkflowsApprovalsCreatePage /> for Workflow Creation #46798

Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
88e2519
Test navigation
blazejkustra Aug 5, 2024
b0878c5
Fix navigating between screens for create flow
blazejkustra Aug 5, 2024
66c99d3
Attach API action
blazejkustra Aug 5, 2024
a0c289b
Merge branch 'Guccio163/45951_implementApprovalWorkflowSection' into …
blazejkustra Aug 5, 2024
11372db
Fix small mistakes and old comments
blazejkustra Aug 5, 2024
d243bd5
Add translations
blazejkustra Aug 6, 2024
d1f0dce
Add one more translation
blazejkustra Aug 6, 2024
5108e12
Add an option to parse text in formHelpMessage component
blazejkustra Aug 6, 2024
062e45a
Adjust MenuItem component to allow parsing text in formHelpMessage co…
blazejkustra Aug 6, 2024
c0734c8
Fix minor errors
blazejkustra Aug 6, 2024
89f6ee0
Create a new type for ApprovalWorkflow that is kept in onyx
blazejkustra Aug 6, 2024
8405648
Take care of all edge cases and error handling
blazejkustra Aug 6, 2024
3a39deb
Allow to deselect an approver
blazejkustra Aug 6, 2024
c4da7e3
Clean the expenses from page logic
blazejkustra Aug 6, 2024
dbb4311
Create an ApprovalWorkflowEditor and adjust CreatePage logic
blazejkustra Aug 6, 2024
e92b654
Merge branch 'main' into approval-workflows/create-edit-screen
blazejkustra Aug 6, 2024
d4ae460
Bring back Permissions.ts
blazejkustra Aug 6, 2024
77555f3
Remove backTo from WORKSPACE_WORKFLOWS_APPROVALS_NEW screen
blazejkustra Aug 7, 2024
1c6ae4b
Fix tests
blazejkustra Aug 7, 2024
31d8863
Do final corrections
blazejkustra Aug 7, 2024
faee4e3
Merge branch 'main' into approval-workflows/create-edit-screen
blazejkustra Aug 7, 2024
ebd1e05
Fix displaying errors and sorting default workflow
blazejkustra Aug 7, 2024
9fa7889
Fix lint
blazejkustra Aug 7, 2024
69c31e2
Change util name to calculateApprovers, fix that circular approver wa…
blazejkustra Aug 7, 2024
b67e8b8
Address comments after initial review
blazejkustra Aug 7, 2024
ba8a3d3
Merge branch 'main' into approval-workflows/create-edit-screen
blazejkustra Aug 7, 2024
755954c
Fix copilot mistake 🙄
blazejkustra Aug 7, 2024
603b6ff
Adjust a comment
blazejkustra Aug 7, 2024
230080e
Remove role from CreateWorkspaceApprovalParams
blazejkustra Aug 7, 2024
54453fb
Filter out the default approver when selecting the first approver for…
blazejkustra Aug 8, 2024
b9b7937
Fix how form errors are displayed
blazejkustra Aug 8, 2024
68d5caf
Fix lint
blazejkustra Aug 8, 2024
64e7f0b
Improve error handling
blazejkustra Aug 8, 2024
cd36817
Implement the logic similar to other function
blazejkustra Aug 8, 2024
436457b
Merge branch 'main' into approval-workflows/create-edit-screen
blazejkustra Aug 9, 2024
4c0078a
Merge branch 'main' into approval-workflows/create-edit-screen
blazejkustra Aug 12, 2024
17cab91
Use const for flow and mode options
blazejkustra Aug 12, 2024
f554da6
Clean the logic for displaying hints
blazejkustra Aug 12, 2024
6707500
Fix that additionalApprover error is cleared
blazejkustra Aug 12, 2024
c38d5d7
Revert "Clean the logic for displaying hints"
blazejkustra Aug 12, 2024
d74ed36
Improve isInMultipleWorkflows logic
blazejkustra Aug 12, 2024
f9ea365
Rename consts
blazejkustra Aug 13, 2024
9bde486
Enable submit button when offline
blazejkustra Aug 13, 2024
e732c6c
Implement offline pattern B
blazejkustra Aug 13, 2024
05c552f
Remove redundant code
blazejkustra Aug 13, 2024
914bb1f
Fix code formatting
blazejkustra Aug 13, 2024
30d8a6a
Remove unnecessary test case
blazejkustra Aug 13, 2024
14f6eb2
Move key further up
blazejkustra Aug 13, 2024
69ab537
Merge branch 'main' into approval-workflows/create-edit-screen
blazejkustra Aug 13, 2024
5f3f64f
Rename variables
blazejkustra Aug 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number;
[ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number;
[ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[];
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow;
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
Expand Down
8 changes: 4 additions & 4 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,20 +624,20 @@ const ROUTES = {
},
WORKSPACE_WORKFLOWS_APPROVALS_NEW: {
route: 'settings/workspaces/:policyID/workflows/approvals/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows/approvals/new` as const,
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/workflows/approvals/new` as const, backTo),
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
},
WORKSPACE_WORKFLOWS_APPROVALS_EDIT: {
route: 'settings/workspaces/:policyID/workflows/approvals/:firstApproverEmail/edit',
getRoute: (policyID: string, firstApproverEmail: string) => `settings/workspaces/${policyID}/workflows/approvals/${encodeURIComponent(firstApproverEmail)}/edit` as const,
},
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',
Expand Down
26 changes: 23 additions & 3 deletions src/components/FormHelpMessage.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -23,11 +25,29 @@ type FormHelpMessageProps = {

/** Whether to show dot indicator */
shouldShowRedDotIndicator?: boolean;

/** Whether should render error text as HTML or as Text */
shouldParseText?: boolean;
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
};

function FormHelpMessage({message = '', children, isError = true, style, shouldShowRedDotIndicator = true}: FormHelpMessageProps) {
function FormHelpMessage({message = '', children, isError = true, style, shouldShowRedDotIndicator = true, shouldParseText = false}: FormHelpMessageProps) {
const theme = useTheme();
const styles = useThemeStyles();

const processedText = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to getHTMLMessage

Copy link
Contributor Author

@blazejkustra blazejkustra Aug 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to HTMLMessage (as this is not a function but a memo)

if (typeof message !== 'string' || !shouldParseText) {
return '';
}

const replacedText = Parser.replace(message, {shouldEscapeText: false});

if (isError) {
return `<alert-text>${replacedText}</alert-text>`;
}

return `<muted-text-label>${replacedText}</muted-text-label>`;
}, [isError, message, shouldParseText]);

if (isEmpty(message) && isEmpty(children)) {
return null;
}
Expand All @@ -41,7 +61,7 @@ function FormHelpMessage({message = '', children, isError = true, style, shouldS
/>
)}
<View style={[styles.flex1, isError && shouldShowRedDotIndicator ? styles.ml2 : {}]}>
{children ?? <Text style={[isError ? styles.formError : styles.formHelp, styles.mb0]}>{message}</Text>}
{children ?? shouldParseText ? <RenderHTML html={processedText} /> : <Text style={[isError ? styles.formError : styles.formHelp, styles.mb0]}>{message}</Text>}
</View>
</View>
);
Expand Down
10 changes: 10 additions & 0 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,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 */
shouldParseHintText?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename these to shouldRenderHintAsHTML and shouldRenderErrorAsHTML

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other props in this component that are named similary, are we sure we want to name it differently?

shouldParseTitle = false,
shouldParseHelperText = false,
shouldParseHintText = false,
shouldParseErrorText = false,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think having them more explicit is better than consistency. In fact, I'd suggest renaming all of them in order to really clean it up.

As an external consumer of the component, I wouldn't really have any idea what "parse text" implies and I'd have to dig into the component and look at the code to try to understand it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but to be clear, I'm not asking you to rename the other props in this PR :D Maybe you or I could do a followup PR to clean those up later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do it in a separate PR 👍


/** Whether should render error text as HTML or as Text */
shouldParseErrorText?: boolean;

/** Should check anonymous user in onPress function */
shouldCheckActionAllowedOnPress?: boolean;

Expand Down Expand Up @@ -387,6 +393,8 @@ function MenuItem(
shouldBlockSelection = false,
shouldParseTitle = false,
shouldParseHelperText = false,
shouldParseHintText = false,
shouldParseErrorText = false,
shouldCheckActionAllowedOnPress = true,
onSecondaryInteraction,
titleWithTooltips,
Expand Down Expand Up @@ -792,6 +800,7 @@ function MenuItem(
shouldShowRedDotIndicator={!!shouldShowRedDotIndicator}
message={errorText}
style={[styles.menuItemError, errorTextStyle]}
shouldParseText={shouldParseErrorText}
/>
)}
{!!hintText && (
Expand All @@ -800,6 +809,7 @@ function MenuItem(
shouldShowRedDotIndicator={false}
message={hintText}
style={styles.menuItemError}
shouldParseText={shouldParseHintText}
/>
)}
</View>
Expand Down
10 changes: 9 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
Expand Down Expand Up @@ -1291,14 +1292,21 @@ export default {
/* eslint-enable @typescript-eslint/naming-convention */
},
},
approverInMultipleWorkflows: ({name1, name2}: ApprovalWorkflowErrorParams) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazejkustra Do you confirm this translation with the internal team before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`<strong>${name1}</strong> already approves reports to <strong>${name2}</strong> in a separate workflow. If you change this approval relationship, all other workflows will be updated.`,
approverCircularReference: ({name1, name2}: ApprovalWorkflowErrorParams) =>
`<strong>${name1}</strong> already approves reports to <strong>${name2}</strong>. Please choose a different approver to avoid a circular workflow.`,
},
workflowsDelayedSubmissionPage: {
autoReportingErrorMessage: "Delayed submission couldn't be changed. Please try again or contact support.",
autoReportingFrequencyErrorMessage: "Submission frequency couldn't be changed. Please try again or contact support.",
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.',
addApproverRow: 'Additional approver',
submitButton: 'Add workflow',
},
workflowsEditApprovalsPage: {
title: 'Edit approval workflow',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
Expand Down Expand Up @@ -1300,6 +1301,10 @@ export default {
/* eslint-enable @typescript-eslint/naming-convention */
},
},
approverInMultipleWorkflows: ({name1, name2}: ApprovalWorkflowErrorParams) =>
`<strong>${name1}</strong> ya aprueba informes a <strong>${name2}</strong> 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) =>
`<strong>${name1}</strong> ya aprueba informes a <strong>${name2}</strong>. 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.',
Expand All @@ -1308,6 +1313,9 @@ export default {
},
workflowsCreateApprovalsPage: {
title: 'Añadir flujo de aprobación',
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
header: 'Agrega más aprobadores y confirma.',
addApproverRow: 'Añadir aprobador',
submitButton: 'Añadir flujo de trabajo',
},
workflowsEditApprovalsPage: {
title: 'Edicion flujo de aprobación',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ type IssueVirtualCardParams = {
link: string;
};

type ApprovalWorkflowErrorParams = {
name1: string;
name2: string;
};

export type {
AddressLineParams,
AdminCanceledRequestParams,
Expand Down Expand Up @@ -475,4 +480,5 @@ export type {
UnapprovedParams,
RemoveMembersWarningPrompt,
DeleteExpenseTranslationParams,
ApprovalWorkflowErrorParams,
};
4 changes: 1 addition & 3 deletions src/libs/API/parameters/WorkspaceApprovalParams.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import type {PolicyEmployee} from '@src/types/onyx';

type CreateWorkspaceApprovalParams = {
authToken: string;
policyID: string;
employees: PolicyEmployee[];
employees: string;
};
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved

type UpdateWorkspaceApprovalParams = CreateWorkspaceApprovalParams;
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,17 +1111,20 @@ type FullScreenNavigatorParamList = {
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: {
policyID: string;
backTo?: Routes;
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: {
policyID: string;
firstApproverEmail: string;
};
[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;
Expand Down
33 changes: 21 additions & 12 deletions src/libs/WorkflowUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import lodashMapKeys from 'lodash/mapKeys';
import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow';
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 EMPTY_APPROVAL_WORKFLOW: ApprovalWorkflowOnyx = {
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
members: [],
approvers: [],
availableMembers: [],
isDefault: false,
isBeingEdited: false,
flow: 'create',
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
isLoading: false,
};

Expand All @@ -29,8 +30,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<string>();
Expand Down Expand Up @@ -98,14 +99,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);
Expand All @@ -120,6 +120,15 @@ function convertPolicyEmployeesToApprovalWorkflows({employees, defaultApprover,
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)
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
if (!sortedApprovalWorkflows[0].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,
Expand All @@ -144,11 +153,11 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
/**
* Should the workflow be removed from the employees
*/
removeWorkflow?: boolean;
mode: 'create' | 'update' | 'remove';
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
};

/** Convert an approval workflow to a list of policy employees */
function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, removeWorkflow = false}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, mode}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
const updatedEmployeeList: PolicyEmployeeList = {};
const firstApprover = approvalWorkflow.approvers.at(0);

Expand All @@ -164,18 +173,18 @@ function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeLis
const nextApprover = approvalWorkflow.approvers.at(index + 1);
updatedEmployeeList[approver.email] = {
...employeeList[approver.email],
forwardsTo: removeWorkflow ? undefined : nextApprover?.email,
forwardsTo: mode === 'remove' ? '' : nextApprover?.email ?? '',
};
});

approvalWorkflow.members.forEach(({email}) => {
updatedEmployeeList[email] = {
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : employeeList[email]),
submitsTo: removeWorkflow ? undefined : firstApprover.email,
submitsTo: mode === 'remove' ? '' : firstApprover.email ?? '',
};
});

return updatedEmployeeList;
}

export {getApprovalWorkflowApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, EMPTY_APPROVAL_WORKFLOW};
export {calculateApprovers as getApprovalWorkflowApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, EMPTY_APPROVAL_WORKFLOW};
Loading
Loading