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