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

[Advanced Approval Workflows] Remove advanced approval workflows beta #48706

Merged
1 change: 0 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,6 @@ const CONST = {
DEFAULT_ROOMS: 'defaultRooms',
DUPE_DETECTION: 'dupeDetection',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_ADVANCED_APPROVAL: 'workflowsAdvancedApproval',
SPOTNANA_TRAVEL: 'spotnanaTravel',
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
WORKSPACE_FEEDS: 'workspaceFeeds',
Expand Down
4 changes: 2 additions & 2 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,8 @@ const ROUTES = {
},
WORKSPACE_WORKFLOWS_APPROVALS_APPROVER: {
route: 'settings/workspaces/:policyID/workflows/approvals/approver',
getRoute: (policyID: string, approverIndex?: number, backTo?: string) =>
getUrlWithBackToParam(`settings/workspaces/${policyID}/workflows/approvals/approver${approverIndex !== undefined ? `?approverIndex=${approverIndex}` : ''}` as const, backTo),
getRoute: (policyID: string, approverIndex: number, backTo?: string) =>
getUrlWithBackToParam(`settings/workspaces/${policyID}/workflows/approvals/approver?approverIndex=${approverIndex}` as const, backTo),
},
WORKSPACE_WORKFLOWS_PAYER: {
route: 'settings/workspaces/:policyID/workflows/payer',
Expand Down
2 changes: 1 addition & 1 deletion src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ type FullScreenNavigatorParamList = {
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVER]: {
policyID: string;
approverIndex?: number;
approverIndex: number;
backTo?: Routes;
};
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: {
Expand Down
5 changes: 0 additions & 5 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ function canUseP2PDistanceRequests(betas: OnyxEntry<Beta[]>, iouType: IOUType |
return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas) || iouType === CONST.IOU.TYPE.TRACK;
}

function canUseWorkflowsAdvancedApproval(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.WORKFLOWS_ADVANCED_APPROVAL) || canUseAllBetas(betas);
}

function canUseSpotnanaTravel(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas);
}
Expand Down Expand Up @@ -65,7 +61,6 @@ export default {
canUseLinkPreviews,
canUseDupeDetection,
canUseP2PDistanceRequests,
canUseWorkflowsAdvancedApproval,
canUseSpotnanaTravel,
canUseWorkspaceFeeds,
canUseCompanyCardFeeds,
Expand Down
13 changes: 12 additions & 1 deletion src/libs/WorkflowUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
type: ValueOf<typeof CONST.APPROVAL_WORKFLOW.TYPE>;
};

/** Convert an approval workflow to a list of policy employees */
/**
* This function converts an approval workflow into a list of policy employees.
* An optimized list is created that contains only the updated employees to maintain minimal data changes.
*/
function convertApprovalWorkflowToPolicyEmployees({
approvalWorkflow,
previousEmployeeList,
Expand All @@ -221,6 +224,8 @@ function convertApprovalWorkflowToPolicyEmployees({
const nextApprover = approvalWorkflow.approvers.at(index + 1);
const forwardsTo = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : nextApprover?.email ?? '';

// For every approver, we check if the forwardsTo field has changed.
// If it has, we update the employee list with the new forwardsTo value.
if (previousEmployeeList[approver.email]?.forwardsTo === forwardsTo) {
return;
}
Expand All @@ -235,6 +240,8 @@ function convertApprovalWorkflowToPolicyEmployees({
approvalWorkflow.members.forEach(({email}) => {
const submitsTo = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : firstApprover.email ?? '';

// For every member, we check if the submitsTo field has changed.
// If it has, we update the employee list with the new submitsTo value.
if (previousEmployeeList[email]?.submitsTo === submitsTo) {
return;
}
Expand All @@ -246,6 +253,8 @@ function convertApprovalWorkflowToPolicyEmployees({
};
});

// For each member to remove, we update the employee list with submitsTo set to ''
// which will set the submitsTo field to the default approver email on backend.
membersToRemove?.forEach(({email}) => {
updatedEmployeeList[email] = {
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
Expand All @@ -254,6 +263,8 @@ function convertApprovalWorkflowToPolicyEmployees({
};
});

// For each approver to remove, we update the employee list with forwardsTo set to ''
// which will reset the forwardsTo on the backend.
approversToRemove?.forEach(({email}) => {
updatedEmployeeList[email] = {
...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
Expand Down
29 changes: 21 additions & 8 deletions src/libs/actions/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ Onyx.connect({
},
});

let personalDetails: PersonalDetailsList | undefined;
let personalDetailsByEmail: PersonalDetailsList = {};
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
personalDetails = value;
callback: (personalDetails) => {
personalDetailsByEmail = lodashMapKeys(personalDetails, (value, key) => value?.login ?? key);
},
});

Expand All @@ -56,6 +56,11 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
const previousApprovalMode = policy.approvalMode;
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.CREATE});

// If there are no changes to the employees list, we can exit early
if (isEmptyObject(updatedEmployees)) {
return;
}

const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -127,6 +132,7 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
approversToRemove,
});

// If there are no changes to the employees list, we can exit early
if (isEmptyObject(updatedEmployees) && !newDefaultApprover) {
return;
}
Expand Down Expand Up @@ -258,12 +264,13 @@ function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
API.write(WRITE_COMMANDS.REMOVE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}

/** Set the members of the approval workflow that is currently edited */
function setApprovalWorkflowMembers(members: Member[]) {
Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {members, errors: null});
}

/**
* Set the approver at the specified index in the current approval workflow
* Set the approver at the specified index in the approval workflow that is currently edited
* @param approver - The new approver to set
* @param approverIndex - The index of the approver to set
* @param policyID - The ID of the policy
Expand All @@ -280,13 +287,14 @@ function setApprovalWorkflowApprover(approver: Approver, approverIndex: number,

// 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});
approvers.splice(approverIndex, approvers.length, ...additionalApprovers);
}

// Always clear the additional approver error when an approver is added
const errors: Record<string, TranslationPaths | null> = {additionalApprover: null};
// Check for circular references and reset errors

// Check for circular references (approver forwards to themselves) and reset other errors
const updatedApprovers = approvers.map((existingApprover, index) => {
if (!existingApprover) {
return;
Expand All @@ -308,6 +316,7 @@ function setApprovalWorkflowApprover(approver: Approver, approverIndex: number,
Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: updatedApprovers, errors});
}

/** Clear one approver at the specified index in the approval workflow that is currently edited */
function clearApprovalWorkflowApprover(approverIndex: number) {
if (!currentApprovalWorkflow) {
return;
Expand All @@ -319,6 +328,7 @@ function clearApprovalWorkflowApprover(approverIndex: number) {
Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: lodashDropRightWhile(approvers, (approver) => !approver), errors: null});
}

/** Clear all approvers of the approval workflow that is currently edited */
function clearApprovalWorkflowApprovers() {
Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {approvers: []});
}
Expand All @@ -333,6 +343,11 @@ function clearApprovalWorkflow() {

type ApprovalWorkflowOnyxValidated = Omit<ApprovalWorkflowOnyx, 'approvers'> & {approvers: Approver[]};

/**
* Validates the approval workflow and sets the errors on the approval workflow
* @param approvalWorkflow the approval workflow to validate
* @returns true if the approval workflow is valid, false otherwise
*/
function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): approvalWorkflow is ApprovalWorkflowOnyxValidated {
const errors: Record<string, TranslationPaths> = {};

Expand All @@ -355,8 +370,6 @@ function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): appro
}

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

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

Expand Down
38 changes: 5 additions & 33 deletions src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import {useFocusEffect} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useMemo, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import ApprovalWorkflowSection from '@components/ApprovalWorkflowSection';
import ConfirmModal from '@components/ConfirmModal';
import getBankIcon from '@components/Icon/BankIcons';
Expand All @@ -23,7 +22,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getPaymentMethodDescription} from '@libs/PaymentUtils';
import Permissions from '@libs/Permissions';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import {convertPolicyEmployeesToApprovalWorkflows, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils';
Expand All @@ -39,29 +37,22 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Beta} from '@src/types/onyx';
import ToggleSettingOptionRow from './ToggleSettingsOptionRow';
import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow';
import {getAutoReportingFrequencyDisplayNames} from './WorkspaceAutoReportingFrequencyPage';
import type {AutoReportingFrequencyKey} from './WorkspaceAutoReportingFrequencyPage';

type WorkspaceWorkflowsPageOnyxProps = {
/** Beta features list */
betas: OnyxEntry<Beta[]>;
};
type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps<FullScreenNavigatorParamList, typeof SCREENS.WORKSPACE.WORKFLOWS>;
type WorkspaceWorkflowsPageProps = WithPolicyProps & StackScreenProps<FullScreenNavigatorParamList, typeof SCREENS.WORKSPACE.WORKFLOWS>;

function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPageProps) {
function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
const {translate, preferredLocale} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();

const policyApproverEmail = policy?.approver;
const canUseAdvancedApproval = Permissions.canUseWorkflowsAdvancedApproval(betas);
const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const policyApproverName = useMemo(() => PersonalDetailsUtils.getPersonalDetailByEmail(policyApproverEmail ?? '')?.displayName ?? policyApproverEmail, [policyApproverEmail]);
const {approvalWorkflows, availableMembers, usedApproverEmails} = useMemo(
() =>
convertPolicyEmployeesToApprovalWorkflows({
Expand Down Expand Up @@ -170,7 +161,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
onToggle: (isEnabled: boolean) => {
Policy.setWorkspaceApprovalMode(route.params.policyID, policy?.owner ?? '', isEnabled ? CONST.POLICY.APPROVAL_MODE.BASIC : CONST.POLICY.APPROVAL_MODE.OPTIONAL);
},
subMenuItems: canUseAdvancedApproval ? (
subMenuItems: (
<>
{approvalWorkflows.map((workflow, index) => (
<OfflineWithFeedback
Expand All @@ -195,17 +186,6 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
onPress={addApprovalAction}
/>
</>
) : (
<MenuItemWithTopDescription
title={policyApproverName ?? ''}
titleStyle={styles.textNormalThemeText}
descriptionTextStyle={styles.textLabelSupportingNormal}
description={translate('workflowsPage.approver')}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(route.params.policyID))}
shouldShowRightIcon
wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt3, styles.mbn3]}
brickRoadIndicator={hasApprovalError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
),
isActive:
([CONST.POLICY.APPROVAL_MODE.BASIC, CONST.POLICY.APPROVAL_MODE.ADVANCED].some((approvalMode) => approvalMode === policy?.approvalMode) && !hasApprovalError) ?? false,
Expand Down Expand Up @@ -301,12 +281,10 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
translate,
preferredLocale,
onPressAutoReportingFrequency,
canUseAdvancedApproval,
approvalWorkflows,
theme.success,
theme.spinner,
addApprovalAction,
policyApproverName,
isOffline,
isPolicyAdmin,
displayNameForAuthorizedPayer,
Expand Down Expand Up @@ -374,10 +352,4 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr

WorkspaceWorkflowsPage.displayName = 'WorkspaceWorkflowsPage';

export default withPolicy(
withOnyx<WorkspaceWorkflowsPageProps, WorkspaceWorkflowsPageOnyxProps>({
betas: {
key: ONYXKEYS.BETAS,
},
})(WorkspaceWorkflowsPage),
);
export default withPolicy(WorkspaceWorkflowsPage);
Loading
Loading