diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx index 49e509ce533e..17cde0306c50 100644 --- a/src/components/ApprovalWorkflowSection.tsx +++ b/src/components/ApprovalWorkflowSection.tsx @@ -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'; @@ -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')}`, @@ -40,7 +35,7 @@ function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowS @@ -70,7 +65,7 @@ function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowS iconHeight={20} iconWidth={20} iconFill={theme.icon} - onPress={openApprovalsEdit} + onPress={onPress} shouldRemoveBackground /> @@ -88,7 +83,7 @@ function ApprovalWorkflowSection({approvalWorkflow, policyID}: ApprovalWorkflowS iconHeight={20} iconWidth={20} iconFill={theme.icon} - onPress={openApprovalsEdit} + onPress={onPress} shouldRemoveBackground /> diff --git a/src/languages/en.ts b/src/languages/en.ts index 27c666caaaf8..7e9760edd37d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -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', diff --git a/src/languages/es.ts b/src/languages/es.ts index e57271de4923..5f983a39991e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -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', diff --git a/src/libs/API/parameters/WorkspaceApprovalParams.ts b/src/libs/API/parameters/WorkspaceApprovalParams.ts index 703d4976c045..0a84bc552bc4 100644 --- a/src/libs/API/parameters/WorkspaceApprovalParams.ts +++ b/src/libs/API/parameters/WorkspaceApprovalParams.ts @@ -12,7 +12,9 @@ type CreateWorkspaceApprovalParams = { employees: string; }; -type UpdateWorkspaceApprovalParams = CreateWorkspaceApprovalParams; +type UpdateWorkspaceApprovalParams = CreateWorkspaceApprovalParams & { + defaultApprover?: string; +}; type RemoveWorkspaceApprovalParams = CreateWorkspaceApprovalParams; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index f4e766517a0a..2b567d27e280 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -525,6 +525,9 @@ const config: LinkingOptions['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, diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 71348368e0d5..d9a3782987f6 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -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 @@ -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); @@ -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; } diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts index 23158308ddd0..0b6c29c9890a 100644 --- a/src/libs/actions/Workflow.ts +++ b/src/libs/actions/Workflow.ts @@ -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({ @@ -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[] = [ { @@ -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[] = [ { @@ -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} : {}), }, }, ]; @@ -150,6 +154,7 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork value: { employeeList: previousEmployeeList, pendingFields: {employeeList: null}, + ...(newDefaultApprover ? {approver: previousDefaultApprover} : {}), }, }, ]; @@ -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}); } @@ -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[] = [ { @@ -324,7 +335,9 @@ function clearApprovalWorkflow() { Onyx.set(ONYXKEYS.APPROVAL_WORKFLOW, null); } -function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): Record { +type ApprovalWorkflowOnyxValidated = Omit & {approvers: Approver[]}; + +function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): approvalWorkflow is ApprovalWorkflowOnyxValidated { const errors: Record = {}; approvalWorkflow.approvers.forEach((approver, approverIndex) => { @@ -337,7 +350,7 @@ function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): Recor } }); - if (!approvalWorkflow.members.length) { + if (!approvalWorkflow.members.length && !approvalWorkflow.isDefault) { errors.members = 'common.error.fieldRequired'; } @@ -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 { diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 4460e1c32961..18b1314c9b1e 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -108,6 +108,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr ); return; } + Workflow.setApprovalWorkflow({ ...INITIAL_APPROVAL_WORKFLOW, availableMembers: approvalWorkflows.at(0)?.members ?? [], @@ -178,7 +179,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr > Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, workflow.approvers[0].email))} /> ))} diff --git a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx index 5c060a0e2fba..a0b9beb5bec4 100644 --- a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx +++ b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx @@ -3,24 +3,39 @@ 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 type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {ApprovalWorkflowOnyx} from '@src/types/onyx'; +import type {ApprovalWorkflowOnyx, Policy} from '@src/types/onyx'; import type {Approver} from '@src/types/onyx/ApprovalWorkflow'; type ApprovalWorkflowEditorProps = { + /** The approval workflow to display */ approvalWorkflow: ApprovalWorkflowOnyx; + + /** Function to remove the approval workflow */ + removeApprovalWorkflow?: () => void; + + /** The policy for the current route */ + policy: OnyxEntry; + + /** The policy ID */ policyID: string; }; -function ApprovalWorkflowEditor({approvalWorkflow, policyID}: ApprovalWorkflowEditorProps, ref: ForwardedRef) { +function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, policy, policyID}: ApprovalWorkflowEditorProps, ref: ForwardedRef) { const styles = useThemeStyles(); + const theme = useTheme(); const {translate, toLocaleOrdinal} = useLocalize(); const approverDescription = useCallback( @@ -67,24 +82,49 @@ function ApprovalWorkflowEditor({approvalWorkflow, policyID}: ApprovalWorkflowEd [approvalWorkflow.approvers, translate], ); + const editMembers = useCallback(() => { + const backTo = approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE ? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID) : undefined; + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(policyID, backTo)); + }, [approvalWorkflow.action, policyID]); + + const editApprover = useCallback( + (approverIndex: number) => { + const backTo = approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE ? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID) : undefined; + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverIndex, backTo)); + }, + [approvalWorkflow.action, policyID], + ); + + // User should be allowed to add additional approver only if they upgraded to Control Plan, otherwise redirected to the Upgrade Page + const addAdditionalApprover = useCallback(() => { + if (!PolicyUtils.isControlPolicy(policy)) { + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.alias, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID))); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approvalWorkflow.approvers.length, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID))); + }, [approvalWorkflow.approvers.length, policy, policyID]); + return ( - {translate('workflowsCreateApprovalsPage.header')} + {approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && ( + {translate('workflowsCreateApprovalsPage.header')} + )} Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(policyID, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)))} - shouldShowRightIcon + onPress={editMembers} wrapperStyle={[styles.sectionMenuItemTopDescription]} errorText={approvalWorkflow?.errors?.members ? translate(approvalWorkflow.errors.members) : undefined} brickRoadIndicator={approvalWorkflow?.errors?.members ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon={!approvalWorkflow.isDefault} + interactive={!approvalWorkflow.isDefault} /> {approvalWorkflow.approvers.map((approver, approverIndex) => { @@ -99,11 +139,7 @@ function ApprovalWorkflowEditor({approvalWorkflow, policyID}: ApprovalWorkflowEd wrapperStyle={styles.sectionMenuItemTopDescription} description={approverDescription(approverIndex)} descriptionTextStyle={!!approver?.displayName && styles.textLabelSupportingNormal} - onPress={() => - Navigation.navigate( - ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverIndex, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)), - ) - } + onPress={() => editApprover(approverIndex)} shouldShowRightIcon hintText={hintText} shouldRenderHintAsHTML @@ -116,16 +152,22 @@ function ApprovalWorkflowEditor({approvalWorkflow, policyID}: ApprovalWorkflowEd - Navigation.navigate( - ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approvalWorkflow.approvers.length, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)), - ) - } + onPress={addAdditionalApprover} shouldShowRightIcon wrapperStyle={styles.sectionMenuItemTopDescription} errorText={approvalWorkflow?.errors?.additionalApprover ? translate(approvalWorkflow.errors.additionalApprover) : undefined} brickRoadIndicator={approvalWorkflow?.errors?.additionalApprover ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> + + {removeApprovalWorkflow && !approvalWorkflow.isDefault && ( + + )} ); diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index fc9274db4b06..545c09f75313 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -188,7 +188,7 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID), CONST.NAVIGATION.TYPE.UP); } else { const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? ''; - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); + Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover)); } }, [approvalWorkflow, approverIndex, personalDetails, policy?.employeeList, route.params.policyID, selectedApproverEmail]); @@ -242,7 +242,9 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i title={translate('workflowsPage.approver')} onBackButtonPress={goBack} /> - {translate('workflowsApproverPage.header')} + {approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && ( + {translate('workflowsApproverPage.header')} + )} (null); // eslint-disable-next-line rulesdir/no-negated-variables @@ -41,11 +40,11 @@ function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = tr return; } - if (!isEmptyObject(Workflow.validateApprovalWorkflow(approvalWorkflow))) { + if (!Workflow.validateApprovalWorkflow(approvalWorkflow)) { return; } - Workflow.createApprovalWorkflow(route.params.policyID, {...approvalWorkflow, approvers: approvalWorkflow.approvers as Approver[]}); + Workflow.createApprovalWorkflow(route.params.policyID, approvalWorkflow); Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)); }, [approvalWorkflow, route.params.policyID]); @@ -68,26 +67,28 @@ function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = tr title={translate('workflowsCreateApprovalsPage.title')} onBackButtonPress={Navigation.goBack} /> - {approvalWorkflowMetadata.status === 'loading' && } {approvalWorkflow && ( - + <> + + { + formRef.current?.scrollTo({y: 0, animated: true}); + }} + isLoading={approvalWorkflow?.isLoading} + buttonText={translate('workflowsCreateApprovalsPage.submitButton')} + containerStyles={[styles.mb5, styles.mh5]} + enabledWhenOffline + /> + )} - - { - formRef.current?.scrollTo({y: 0, animated: true}); - }} - isLoading={approvalWorkflow?.isLoading} - buttonText={translate('workflowsCreateApprovalsPage.submitButton')} - containerStyles={[styles.mb5, styles.mh5]} - enabledWhenOffline - /> + {!approvalWorkflow && } diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx index 8b55831b276c..25f288690c5d 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx @@ -1,26 +1,95 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useCallback, useEffect, useRef, useState} 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 ConfirmModal from '@components/ConfirmModal'; +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 {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; 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 type SCREENS from '@src/SCREENS'; +import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import ApprovalWorkflowEditor from './ApprovalWorkflowEditor'; type WorkspaceWorkflowsApprovalsEditPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsEditPageProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [approvalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); + const [initialApprovalWorkflow, setInitialApprovalWorkflow] = useState(); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const formRef = useRef(null); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy) || !approvalWorkflow; + + const updateApprovalWorkflow = useCallback(() => { + if (!approvalWorkflow || !initialApprovalWorkflow) { + return; + } + + if (!Workflow.validateApprovalWorkflow(approvalWorkflow)) { + return; + } + + const membersToRemove = initialApprovalWorkflow.members.filter((initialMember) => !approvalWorkflow.members.some((member) => member.email === initialMember.email)); + Workflow.updateApprovalWorkflow(route.params.policyID, approvalWorkflow, membersToRemove); + Navigation.goBack(); + }, [approvalWorkflow, initialApprovalWorkflow, route.params.policyID]); + + const removeApprovalWorkflow = useCallback(() => { + if (!initialApprovalWorkflow) { + return; + } + + // Remove the approval workflow using the initial data as it could be already edited + Workflow.removeApprovalWorkflow(route.params.policyID, initialApprovalWorkflow); + Navigation.goBack(); + }, [initialApprovalWorkflow, route.params.policyID]); + + // Set the initial approval workflow when the page is loaded + useEffect(() => { + if (!!initialApprovalWorkflow || !policy || !personalDetails) { + return; + } + + const defaultApprover = policy?.approver ?? policy.owner; + const workflows = convertPolicyEmployeesToApprovalWorkflows({ + employees: policy.employeeList ?? {}, + defaultApprover, + personalDetails, + }); + const currentApprovalWorkflow = workflows.find((workflow) => workflow.approvers.at(0)?.email === route.params.firstApproverEmail); + + if (!currentApprovalWorkflow) { + return Workflow.clearApprovalWorkflow(); + } + + Workflow.setApprovalWorkflow({ + ...currentApprovalWorkflow, + availableMembers: [...currentApprovalWorkflow.members, ...(workflows.at(0)?.members ?? [])], + action: CONST.APPROVAL_WORKFLOW.ACTION.EDIT, + isLoading: false, + }); + setInitialApprovalWorkflow(currentApprovalWorkflow); + }, [initialApprovalWorkflow, personalDetails, policy, route.params.firstApproverEmail, route.params.policyID]); return ( + {approvalWorkflow && ( + <> + setIsDeleteModalVisible(true)} + policy={policy} + policyID={route.params.policyID} + ref={formRef} + /> + { + formRef.current?.scrollTo({y: 0, animated: true}); + }} + isLoading={approvalWorkflow?.isLoading} + buttonText={translate('common.save')} + containerStyles={[styles.mb5, styles.mh5]} + enabledWhenOffline + /> + + )} + {!initialApprovalWorkflow && } + setIsDeleteModalVisible(false)} + prompt={translate('workflowsEditApprovalsPage.deletePrompt')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> ); diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx index 9b1e0d7f3c23..413a8787ee34 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx @@ -68,7 +68,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat approvalWorkflow.members.map((member) => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); const accountID = Number(policyMemberEmailsToAccountIDs[member.email] ?? ''); - const isAdmin = policy?.employeeList?.[member.email].role === CONST.REPORT.ROLE.ADMIN; + const isAdmin = policy?.employeeList?.[member.email]?.role === CONST.REPORT.ROLE.ADMIN; return { text: member.displayName, @@ -89,7 +89,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat if (approvalWorkflow?.availableMembers) { const availableMembers = approvalWorkflow.availableMembers .map((member) => { - const isAdmin = policy?.employeeList?.[member.email].role === CONST.REPORT.ROLE.ADMIN; + const isAdmin = policy?.employeeList?.[member.email]?.role === CONST.REPORT.ROLE.ADMIN; const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); const accountID = Number(policyMemberEmailsToAccountIDs[member.email] ?? ''); @@ -190,7 +190,9 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat title={translate('workflowsExpensesFromPage.title')} onBackButtonPress={goBack} /> - {translate('workflowsExpensesFromPage.header')} + {approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && ( + {translate('workflowsExpensesFromPage.header')} + )} = {}): PolicyEmployee { return { email: `${accountID}@example.com`, - role: 'user', ...policyEmployee, }; } @@ -385,16 +384,12 @@ describe('WorkflowUtils', () => { approvers: [buildApprover(1)], isDefault: true, }; - 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, type: 'create'}); + const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: 'create'}); expect(convertedEmployees).toEqual({ - '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'}), + '1@example.com': buildPolicyEmployee(1, {forwardsTo: '', submitsTo: '1@example.com'}), + '2@example.com': buildPolicyEmployee(2, {submitsTo: '1@example.com'}), }); }); @@ -404,24 +399,16 @@ describe('WorkflowUtils', () => { approvers: [buildApprover(1, {forwardsTo: '2@example.com'}), buildApprover(2, {forwardsTo: '2@example.com'}), buildApprover(3)], isDefault: 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'}), - '5@example.com': buildPolicyEmployee(5, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}), - '6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}), - }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type: 'create'}); + const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, 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: '', 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'}), + '1@example.com': buildPolicyEmployee(1, {forwardsTo: '2@example.com'}), + '2@example.com': buildPolicyEmployee(2, {forwardsTo: '3@example.com'}), + '3@example.com': buildPolicyEmployee(3, {forwardsTo: ''}), + '4@example.com': buildPolicyEmployee(4, {submitsTo: '1@example.com'}), + '5@example.com': buildPolicyEmployee(5, {submitsTo: '1@example.com'}), + '6@example.com': buildPolicyEmployee(6, {submitsTo: '1@example.com'}), }); }); @@ -431,24 +418,16 @@ describe('WorkflowUtils', () => { approvers: [buildApprover(1, {forwardsTo: '2@example.com'}), buildApprover(2, {forwardsTo: '2@example.com'}), buildApprover(3)], isDefault: 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'}), - '5@example.com': buildPolicyEmployee(5, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}), - '6@example.com': buildPolicyEmployee(6, {forwardsTo: 'previous@example.com', submitsTo: 'previous@example.com'}), - }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, employeeList, type: 'remove'}); + const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: 'remove'}); expect(convertedEmployees).toEqual({ - '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: ''}), + '1@example.com': buildPolicyEmployee(1, {forwardsTo: ''}), + '2@example.com': buildPolicyEmployee(2, {forwardsTo: ''}), + '3@example.com': buildPolicyEmployee(3, {forwardsTo: ''}), + '4@example.com': buildPolicyEmployee(4, {submitsTo: ''}), + '5@example.com': buildPolicyEmployee(5, {submitsTo: ''}), + '6@example.com': buildPolicyEmployee(6, {submitsTo: ''}), }); }); });