Skip to content

Commit

Permalink
Merge pull request #46798 from software-mansion-labs/approval-workflo…
Browse files Browse the repository at this point in the history
…ws/create-edit-screen

[CRITICAL] Implement <WorkspaceWorkflowsApprovalsCreatePage /> for Workflow Creation
  • Loading branch information
tgolen authored Aug 13, 2024
2 parents c5feb89 + 5f3f64f commit c010e8c
Show file tree
Hide file tree
Showing 22 changed files with 594 additions and 172 deletions.
12 changes: 12 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions src/components/ApprovalWorkflowSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
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 */
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 `<alert-text>${replacedText}</alert-text>`;
}

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

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 ?? (shouldRenderMessageAsHTML ? <RenderHTML html={HTMLMessage} /> : <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 @@ -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;

Expand Down Expand Up @@ -394,6 +400,8 @@ function MenuItem(
shouldBlockSelection = false,
shouldParseTitle = false,
shouldParseHelperText = false,
shouldRenderHintAsHTML = false,
shouldRenderErrorAsHTML = false,
shouldCheckActionAllowedOnPress = true,
onSecondaryInteraction,
titleWithTooltips,
Expand Down Expand Up @@ -802,6 +810,7 @@ function MenuItem(
shouldShowRedDotIndicator={!!shouldShowRedDotIndicator}
message={errorText}
style={[styles.menuItemError, errorTextStyle]}
shouldRenderMessageAsHTML={shouldRenderErrorAsHTML}
/>
)}
{!!hintText && (
Expand All @@ -810,6 +819,7 @@ function MenuItem(
shouldShowRedDotIndicator={false}
message={hintText}
style={styles.menuItemError}
shouldRenderMessageAsHTML={shouldRenderHintAsHTML}
/>
)}
</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 @@ -1310,14 +1311,21 @@ export default {
/* eslint-enable @typescript-eslint/naming-convention */
},
},
approverInMultipleWorkflows: ({name1, name2}: ApprovalWorkflowErrorParams) =>
`<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.',
additionalApprover: 'Additional approver',
submitButton: 'Add workflow',
},
workflowsEditApprovalsPage: {
title: 'Edit approval workflow',
Expand Down
10 changes: 9 additions & 1 deletion 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 @@ -1319,14 +1320,21 @@ 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.',
autoReportingFrequencyErrorMessage: 'La frecuencia de envío no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.',
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',
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,
};
12 changes: 9 additions & 3 deletions src/libs/API/parameters/WorkspaceApprovalParams.ts
Original file line number Diff line number Diff line change
@@ -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;
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 @@ -1123,17 +1123,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
46 changes: 29 additions & 17 deletions src/libs/WorkflowUtils.ts
Original file line number Diff line number Diff line change
@@ -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,
};

Expand All @@ -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<string>();
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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<typeof CONST.APPROVAL_WORKFLOW.TYPE>;
};

/** 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);

Expand All @@ -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};
Loading

0 comments on commit c010e8c

Please sign in to comment.