Skip to content

Commit

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

[CRITICAL] Implement <WorkspaceWorkflowsApprovalsEditPage /> for Editing Workflow
  • Loading branch information
rlinoz authored Aug 14, 2024
2 parents 29fb6bb + d18d2f0 commit 5cd5c16
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 110 deletions.
19 changes: 7 additions & 12 deletions src/components/ApprovalWorkflowSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import ROUTES from '@src/ROUTES';
import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
Expand All @@ -17,19 +15,16 @@ type ApprovalWorkflowSectionProps = {
/** Single workflow displayed in this component */
approvalWorkflow: ApprovalWorkflow;

/** ID of the policy */
policyID: string;
/** A function that is called when the section is pressed */
onPress: () => void;
};

function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowSectionProps) {
function ApprovalWorkflowSection({approvalWorkflow, onPress}: 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],
);

const approverTitle = useCallback(
(index: number) =>
approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`,
Expand All @@ -40,7 +35,7 @@ function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowS
<PressableWithoutFeedback
accessibilityRole="button"
style={[styles.border, isSmallScreenWidth ? styles.p3 : styles.p4, styles.flexRow, styles.justifyContentBetween, styles.mt6, styles.mbn3]}
onPress={openApprovalsEdit}
onPress={onPress}
accessibilityLabel={translate('workflowsPage.addApprovalsTitle')}
>
<View style={[styles.flex1]}>
Expand Down Expand Up @@ -70,7 +65,7 @@ function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowS
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
onPress={openApprovalsEdit}
onPress={onPress}
shouldRemoveBackground
/>

Expand All @@ -88,7 +83,7 @@ function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowS
iconHeight={20}
iconWidth={20}
iconFill={theme.icon}
onPress={openApprovalsEdit}
onPress={onPress}
shouldRemoveBackground
/>
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,8 @@ export default {
},
workflowsEditApprovalsPage: {
title: 'Edit approval workflow',
deleteTitle: 'Delete approval workflow',
deletePrompt: 'Are you sure you want to delete this approval workflow? All members will subsequently follow the default workflow.',
},
workflowsExpensesFromPage: {
title: 'Expenses from',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,8 @@ export default {
},
workflowsEditApprovalsPage: {
title: 'Edicion flujo de aprobación',
deleteTitle: 'Eliminar flujo de trabajo de aprobación',
deletePrompt: '¿Estás seguro de que quieres eliminar este flujo de trabajo de aprobación? Todos los miembros pasarán a usar el flujo de trabajo predeterminado.',
},
workflowsExpensesFromPage: {
title: 'Gastos de',
Expand Down
4 changes: 3 additions & 1 deletion src/libs/API/parameters/WorkspaceApprovalParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ type CreateWorkspaceApprovalParams = {
employees: string;
};

type UpdateWorkspaceApprovalParams = CreateWorkspaceApprovalParams;
type UpdateWorkspaceApprovalParams = CreateWorkspaceApprovalParams & {
defaultApprover?: string;
};

type RemoveWorkspaceApprovalParams = CreateWorkspaceApprovalParams;

Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
},
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: {
path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.route,
parse: {
firstApproverEmail: (firstApproverEmail: string) => decodeURIComponent(firstApproverEmail),
},
},
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EXPENSES_FROM]: {
path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.route,
Expand Down
17 changes: 12 additions & 5 deletions src/libs/WorkflowUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
approvalWorkflow: ApprovalWorkflow;

/**
* Current list of employees in the policy
* Members to remove from the approval workflow
*/
employeeList: PolicyEmployeeList;
membersToRemove?: Member[];

/**
* Mode to use when converting the approval workflow
Expand All @@ -164,7 +164,7 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
};

/** Convert an approval workflow to a list of policy employees */
function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, membersToRemove, type}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
const updatedEmployeeList: PolicyEmployeeList = {};
const firstApprover = approvalWorkflow.approvers.at(0);

Expand All @@ -175,18 +175,25 @@ function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeLis
approvalWorkflow.approvers.forEach((approver, index) => {
const nextApprover = approvalWorkflow.approvers.at(index + 1);
updatedEmployeeList[approver.email] = {
...employeeList[approver.email],
email: approver.email,
forwardsTo: type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : nextApprover?.email ?? '',
};
});

approvalWorkflow.members.forEach(({email}) => {
updatedEmployeeList[email] = {
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : employeeList[email]),
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
submitsTo: type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : firstApprover.email ?? '',
};
});

membersToRemove?.forEach(({email}) => {
updatedEmployeeList[email] = {
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
submitsTo: '',
};
});

return updatedEmployeeList;
}

Expand Down
33 changes: 24 additions & 9 deletions src/libs/actions/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
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';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

let currentApprovalWorkflow: ApprovalWorkflowOnyx | undefined;
Onyx.connect({
Expand Down Expand Up @@ -53,7 +54,7 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork

const previousEmployeeList = {...policy.employeeList};
const previousApprovalMode = policy.approvalMode;
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, type: CONST.APPROVAL_WORKFLOW.TYPE.CREATE});
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.CREATE});

const optimisticData: OnyxUpdate[] = [
{
Expand Down Expand Up @@ -110,15 +111,17 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}

function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow) {
function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow, membersToRemove: Member[]) {
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];

if (!authToken || !policy) {
return;
}

const previousDefaultApprover = policy.approver ?? policy.owner;
const newDefaultApprover = approvalWorkflow.isDefault ? approvalWorkflow.approvers[0].email : undefined;
const previousEmployeeList = {...policy.employeeList};
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, type: CONST.APPROVAL_WORKFLOW.TYPE.UPDATE});
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.UPDATE, membersToRemove});

const optimisticData: OnyxUpdate[] = [
{
Expand All @@ -134,6 +137,7 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
value: {
employeeList: updatedEmployees,
pendingFields: {employeeList: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
...(newDefaultApprover ? {approver: newDefaultApprover} : {}),
},
},
];
Expand All @@ -150,6 +154,7 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
value: {
employeeList: previousEmployeeList,
pendingFields: {employeeList: null},
...(newDefaultApprover ? {approver: previousDefaultApprover} : {}),
},
},
];
Expand All @@ -169,7 +174,12 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
},
];

const parameters: UpdateWorkspaceApprovalParams = {policyID, authToken, employees: JSON.stringify(Object.values(updatedEmployees))};
const parameters: UpdateWorkspaceApprovalParams = {
policyID,
authToken,
employees: JSON.stringify(Object.values(updatedEmployees)),
defaultApprover: newDefaultApprover,
};
API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}

Expand All @@ -181,11 +191,12 @@ function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
}

const previousEmployeeList = {...policy.employeeList};
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList: previousEmployeeList, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE});
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE});
const updatedEmployeeList = {...previousEmployeeList, ...updatedEmployees};

const defaultApprover = policy.approver ?? policy.owner;
// If there is more than one workflow, we need to keep the advanced approval mode (first workflow is the default)
const hasMoreThanOneWorkflow = Object.values(updatedEmployeeList).some((employee) => !!employee.submitsTo && employee.submitsTo !== policy.approver);
const hasMoreThanOneWorkflow = Object.values(updatedEmployeeList).some((employee) => !!employee.submitsTo && employee.submitsTo !== defaultApprover);

const optimisticData: OnyxUpdate[] = [
{
Expand Down Expand Up @@ -324,7 +335,9 @@ function clearApprovalWorkflow() {
Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, null);
}

function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): Record<string, TranslationPaths> {
type ApprovalWorkflowOnyxValidated = Omit<ApprovalWorkflowOnyx, 'approvers'> & {approvers: Approver[]};

function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): approvalWorkflow is ApprovalWorkflowOnyxValidated {
const errors: Record<string, TranslationPaths> = {};

approvalWorkflow.approvers.forEach((approver, approverIndex) => {
Expand All @@ -337,7 +350,7 @@ function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): Recor
}
});

if (!approvalWorkflow.members.length) {
if (!approvalWorkflow.members.length && !approvalWorkflow.isDefault) {
errors.members = 'common.error.fieldRequired';
}

Expand All @@ -346,7 +359,9 @@ function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): Recor
}

Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {errors});
return errors;

// Return false if there are errors
return isEmptyObject(errors);
}

export {
Expand Down
3 changes: 2 additions & 1 deletion src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
);
return;
}

Workflow.setApprovalWorkflow({
...INITIAL_APPROVAL_WORKFLOW,
availableMembers: approvalWorkflows.at(0)?.members ?? [],
Expand Down Expand Up @@ -178,7 +179,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
>
<ApprovalWorkflowSection
approvalWorkflow={workflow}
policyID={route.params.policyID}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, workflow.approvers[0].email))}
/>
</OfflineWithFeedback>
))}
Expand Down
Loading

0 comments on commit 5cd5c16

Please sign in to comment.