diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 2178b52d1c1c..e86e4aa755f9 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -443,6 +443,7 @@ jobs:
IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }}
IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED }}
needs: [android, desktop, iOS, web]
+ if: ${{ always() }}
steps:
- name: Check deployment success on at least one platform
id: checkDeploymentSuccess
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 04698c00903b..ee777df0571a 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009003100
- versionName "9.0.31-0"
+ versionCode 1009003101
+ versionName "9.0.31-1"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index d939fc0a4bed..9330517bb728 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.31.0
+ 9.0.31.1
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 0f5131ebded5..5c6b778135af 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.31.0
+ 9.0.31.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 3e4c510c61d2..dc11ac79e7d3 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.31
CFBundleVersion
- 9.0.31.0
+ 9.0.31.1
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 0b924f3334d0..b5a4cb34d4fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.31-0",
+ "version": "9.0.31-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.31-0",
+ "version": "9.0.31-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 58293174b968..975142173245 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.31-0",
+ "version": "9.0.31-1",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 9e8f20edb2c9..bdccecded23e 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -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',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 3c488ec40467..61444f4bad33 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -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',
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index ca1324a31fa7..e7a8ac039669 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -1,8 +1,9 @@
import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx, withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
+import Avatar from '@components/Avatar';
import Checkbox from '@components/Checkbox';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -73,8 +74,8 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? ''));
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? -1;
- const htmlForTaskPreview =
- taskAssigneeAccountID > 0 ? ` ${taskTitle}` : `${taskTitle}`;
+ const [avatar] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (personalDetails) => personalDetails?.[taskAssigneeAccountID]?.avatar});
+ const htmlForTaskPreview = `${taskTitle}`;
const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action);
if (isDeletedParentAction) {
@@ -94,7 +95,7 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR
accessibilityLabel={translate('task.task')}
>
-
+
- ${htmlForTaskPreview}` : htmlForTaskPreview} />
+ {taskAssigneeAccountID > 0 && (
+
+ )}
+
+ ${htmlForTaskPreview}` : htmlForTaskPreview} />
+
, iouType: IOUType |
return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas) || iouType === CONST.IOU.TYPE.TRACK;
}
-function canUseWorkflowsAdvancedApproval(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.WORKFLOWS_ADVANCED_APPROVAL) || canUseAllBetas(betas);
-}
-
function canUseSpotnanaTravel(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas);
}
@@ -65,7 +61,6 @@ export default {
canUseLinkPreviews,
canUseDupeDetection,
canUseP2PDistanceRequests,
- canUseWorkflowsAdvancedApproval,
canUseSpotnanaTravel,
canUseWorkspaceFeeds,
canUseCompanyCardFeeds,
diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts
index 4682654d3583..7d936bff0b3b 100644
--- a/src/libs/WorkflowUtils.ts
+++ b/src/libs/WorkflowUtils.ts
@@ -200,7 +200,10 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
type: ValueOf;
};
-/** 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,
@@ -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;
}
@@ -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;
}
@@ -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}),
@@ -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}),
diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts
index 4adb692919ee..5e4b0408f155 100644
--- a/src/libs/actions/Workflow.ts
+++ b/src/libs/actions/Workflow.ts
@@ -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);
},
});
@@ -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,
@@ -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;
}
@@ -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
@@ -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 = {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;
@@ -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;
@@ -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: []});
}
@@ -333,6 +343,11 @@ function clearApprovalWorkflow() {
type ApprovalWorkflowOnyxValidated = Omit & {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 = {};
@@ -355,8 +370,6 @@ function validateApprovalWorkflow(approvalWorkflow: ApprovalWorkflowOnyx): appro
}
Onyx.merge(ONYXKEYS.APPROVAL_WORKFLOW, {errors});
-
- // Return false if there are errors
return isEmptyObject(errors);
}
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index 3cc06bede222..e86d70b11c9a 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -100,7 +100,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
required: policyTagList.required,
rightElement: (
tag.enabled) ? translate('common.required') : undefined}
shouldShowCaret={false}
/>
),
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 9c03e5001a28..6f6bd16a7996 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -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';
@@ -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';
@@ -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;
-};
-type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps;
+type WorkspaceWorkflowsPageProps = WithPolicyProps & StackScreenProps;
-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({
@@ -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) => (
>
- ) : (
- 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,
@@ -301,12 +281,10 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
translate,
preferredLocale,
onPressAutoReportingFrequency,
- canUseAdvancedApproval,
approvalWorkflows,
theme.success,
theme.spinner,
addApprovalAction,
- policyApproverName,
isOffline,
isPolicyAdmin,
displayNameForAuthorizedPayer,
@@ -374,10 +352,4 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
WorkspaceWorkflowsPage.displayName = 'WorkspaceWorkflowsPage';
-export default withPolicy(
- withOnyx({
- betas: {
- key: ONYXKEYS.BETAS,
- },
- })(WorkspaceWorkflowsPage),
-);
+export default withPolicy(WorkspaceWorkflowsPage);
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
index b46b7e1697ee..6125ac842537 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx
@@ -1,8 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {SectionListData} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {useOnyx, withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import Badge from '@components/Badge';
import BlockingView from '@components/BlockingViews/BlockingView';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -13,44 +12,29 @@ import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
-import type {ListItem, Section} from '@components/SelectionList/types';
-import UserListItem from '@components/SelectionList/UserListItem';
+import type {Section} from '@components/SelectionList/types';
import Text from '@components/Text';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
-import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
-import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
-import Permissions from '@libs/Permissions';
-import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import variables from '@styles/variables';
-import * as Policy from '@userActions/Policy/Policy';
import * as Workflow from '@userActions/Workflow';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {Beta, PolicyEmployee} from '@src/types/onyx';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-type WorkspaceWorkflowsApprovalsApproverPageOnyxProps = {
- /** Beta features list */
- // eslint-disable-next-line react/no-unused-prop-types -- This prop is used in the component
- betas: OnyxEntry;
-};
-
-type WorkspaceWorkflowsApprovalsApproverPageProps = WorkspaceWorkflowsApprovalsApproverPageOnyxProps &
- WithPolicyAndFullscreenLoadingProps &
+type WorkspaceWorkflowsApprovalsApproverPageProps = WithPolicyAndFullscreenLoadingProps &
StackScreenProps;
type SelectionListApprover = {
@@ -64,17 +48,7 @@ type SelectionListApprover = {
};
type ApproverSection = SectionListData>;
-function WorkspaceWorkflowsApprovalsApproverPageWrapper(props: WorkspaceWorkflowsApprovalsApproverPageProps) {
- if (Permissions.canUseWorkflowsAdvancedApproval(props.betas) && props.route.params.approverIndex !== undefined) {
- // eslint-disable-next-line react/jsx-props-no-spreading
- return ;
- }
-
- // eslint-disable-next-line react/jsx-props-no-spreading
- return ;
-}
-
-function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) {
+function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
@@ -96,11 +70,14 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
setSelectedApproverEmail(currentApprover.email);
}, [approvalWorkflow?.approvers, approverIndex]);
+ const employeeList = policy?.employeeList;
+ const approversFromWorkflow = approvalWorkflow?.approvers;
+ const isDefault = approvalWorkflow?.isDefault;
const sections: ApproverSection[] = useMemo(() => {
const approvers: SelectionListApprover[] = [];
- if (policy?.employeeList) {
- const availableApprovers = Object.values(policy.employeeList)
+ if (employeeList) {
+ const availableApprovers = Object.values(employeeList)
.map((employee): SelectionListApprover | null => {
const isAdmin = employee?.role === CONST.REPORT.ROLE.ADMIN;
const email = employee.email;
@@ -110,17 +87,17 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
}
// Do not allow the same email to be added twice
- const isEmailAlreadyInApprovers = approvalWorkflow?.approvers.some((approver, index) => approver?.email === email && index !== approverIndex);
+ const isEmailAlreadyInApprovers = approversFromWorkflow?.some((approver, index) => approver?.email === email && index !== approverIndex);
if (isEmailAlreadyInApprovers && selectedApproverEmail !== email) {
return null;
}
// Do not allow the default approver to be added as the first approver
- if (!approvalWorkflow?.isDefault && approverIndex === 0 && defaultApprover === email) {
+ if (!isDefault && approverIndex === 0 && defaultApprover === email) {
return null;
}
- const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
+ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(employeeList);
const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
const {avatar, displayName = email} = personalDetails?.[accountID] ?? {};
@@ -148,30 +125,21 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
})
: approvers;
+ const data = OptionsListUtils.sortAlphabetically(filteredApprovers, 'text');
return [
{
title: undefined,
- data: OptionsListUtils.sortAlphabetically(filteredApprovers, 'text'),
+ data,
shouldShow: true,
},
];
- }, [
- approvalWorkflow?.approvers,
- approvalWorkflow?.isDefault,
- approverIndex,
- debouncedSearchTerm,
- defaultApprover,
- personalDetails,
- policy?.employeeList,
- selectedApproverEmail,
- translate,
- ]);
+ }, [approversFromWorkflow, isDefault, approverIndex, debouncedSearchTerm, defaultApprover, personalDetails, employeeList, selectedApproverEmail, translate]);
const shouldShowListEmptyContent = !debouncedSearchTerm && approvalWorkflow && !sections[0].data.length;
const nextStep = useCallback(() => {
if (selectedApproverEmail) {
- const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
+ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(employeeList);
const accountID = Number(policyMemberEmailsToAccountIDs[selectedApproverEmail] ?? '');
const {avatar, displayName = selectedApproverEmail} = personalDetails?.[accountID] ?? {};
Workflow.setApprovalWorkflowApprover(
@@ -193,7 +161,7 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
const firstApprover = approvalWorkflow?.approvers?.[0]?.email ?? '';
Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, firstApprover));
}
- }, [approvalWorkflow, approverIndex, personalDetails, policy?.employeeList, route.params.policyID, selectedApproverEmail]);
+ }, [approvalWorkflow, approverIndex, personalDetails, employeeList, route.params.policyID, selectedApproverEmail]);
const button = useMemo(() => {
let buttonText = isInitialCreationFlow ? translate('common.next') : translate('common.save');
@@ -253,7 +221,7 @@ function WorkspaceWorkflowsApprovalsApproverPageBeta({policy, personalDetails, i
>
& {accountID: number};
-type MembersSection = SectionListData>;
-
-// TODO: Remove this component when workflowsAdvancedApproval beta is removed
-function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) {
- const {translate} = useLocalize();
- const policyName = policy?.name ?? '';
- const [searchTerm, setSearchTerm] = useState('');
- const {isOffline} = useNetwork();
-
- const isDeletedPolicyEmployee = useCallback(
- (policyEmployee: PolicyEmployee) => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors),
- [isOffline],
- );
-
- const [formattedPolicyEmployeeList, formattedApprover] = useMemo(() => {
- const policyMemberDetails: MemberOption[] = [];
- const approverDetails: MemberOption[] = [];
-
- const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList);
-
- Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => {
- if (isDeletedPolicyEmployee(policyEmployee)) {
- return;
- }
-
- const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
-
- const details = personalDetails?.[accountID];
- if (!details) {
- Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
- return;
- }
- const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm);
- if (searchValue.trim() && !OptionsListUtils.isSearchStringMatchUserDetails(details, searchValue)) {
- return;
- }
-
- const isOwner = policy?.owner === details.login;
- const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN;
-
- let roleBadge = null;
- if (isOwner || isAdmin) {
- roleBadge = ;
- }
-
- const formattedMember = {
- keyForList: String(accountID),
- accountID,
- isSelected: policy?.approver === details.login,
- isDisabled: policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors),
- text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)),
- alternateText: formatPhoneNumber(details?.login ?? ''),
- rightElement: roleBadge,
- icons: [
- {
- source: details.avatar ?? FallbackAvatar,
- name: formatPhoneNumber(details?.login ?? ''),
- type: CONST.ICON_TYPE_AVATAR,
- id: accountID,
- },
- ],
- errors: policyEmployee.errors,
- pendingAction: policyEmployee.pendingAction,
- };
-
- if (policy?.approver === details.login) {
- approverDetails.push(formattedMember);
- } else {
- policyMemberDetails.push(formattedMember);
- }
- });
- return [policyMemberDetails, approverDetails];
- }, [policy?.employeeList, policy?.owner, policy?.approver, isDeletedPolicyEmployee, personalDetails, searchTerm, translate]);
-
- const sections: MembersSection[] = useMemo(() => {
- const sectionsArray: MembersSection[] = [];
-
- if (searchTerm !== '') {
- return [
- {
- title: undefined,
- data: [...formattedApprover, ...formattedPolicyEmployeeList],
- shouldShow: true,
- },
- ];
- }
-
- sectionsArray.push({
- title: undefined,
- data: formattedApprover,
- shouldShow: formattedApprover.length > 0,
- });
-
- sectionsArray.push({
- title: translate('common.all'),
- data: formattedPolicyEmployeeList,
- shouldShow: true,
- });
-
- return sectionsArray;
- }, [formattedPolicyEmployeeList, formattedApprover, searchTerm, translate]);
-
- const headerMessage = useMemo(
- () => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''),
-
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- [translate, sections],
- );
-
- const setPolicyApprover = (member: MemberOption) => {
- if (!policy?.approvalMode || !personalDetails?.[member.accountID]?.login) {
- return;
- }
- const approver: string = personalDetails?.[member.accountID]?.login ?? policy.approver ?? policy.owner;
- Policy.setWorkspaceApprovalMode(policy.id, approver, policy.approvalMode);
- Navigation.goBack();
- };
-
- // eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy);
-
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-WorkspaceWorkflowsApprovalsApproverPageWrapper.displayName = 'WorkspaceWorkflowsApprovalsApproverPage';
+WorkspaceWorkflowsApprovalsApproverPage.displayName = 'WorkspaceWorkflowsApprovalsApproverPage';
-export default withPolicyAndFullscreenLoading(
- withOnyx({
- betas: {
- key: ONYXKEYS.BETAS,
- },
- })(WorkspaceWorkflowsApprovalsApproverPageWrapper),
-);
+export default withPolicyAndFullscreenLoading(WorkspaceWorkflowsApprovalsApproverPage);