From 625dad02312f2b35f6762bcf8a6f05ad6715d505 Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Thu, 28 May 2026 09:17:32 -0500 Subject: [PATCH 1/8] feat: enhance approver selection and agent creation workflow - Added a new `headerContent` prop to the ApproverSelectionList component for customizable header content. - Introduced translations for creating new agents in English and Spanish. - Updated AddAgentPage to handle optimistic agent creation and reconciliation within approval workflows. - Enhanced WorkspaceWorkflowsApprovalsApproverPage to conditionally display a menu item for creating new agents, improving user experience in agent management. --- src/components/ApproverSelectionList.tsx | 3 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/pages/settings/Agents/AddAgentPage.tsx | 44 ++++++++++- ...orkspaceWorkflowsApprovalsApproverPage.tsx | 77 ++++++++++++++++++- 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/components/ApproverSelectionList.tsx b/src/components/ApproverSelectionList.tsx index 49c218598329..40023ab96a1f 100644 --- a/src/components/ApproverSelectionList.tsx +++ b/src/components/ApproverSelectionList.tsx @@ -32,6 +32,7 @@ type ApproverSelectionListPageProps = { shouldShowNotFoundViewLink?: boolean; listEmptyContentSubtitle?: string; footerContent?: React.ReactNode; + headerContent?: React.JSX.Element | null; subtitle?: React.ReactNode; shouldShowTextInput?: boolean; allApprovers: SelectionListApprover[]; @@ -60,6 +61,7 @@ function ApproverSelectionList({ shouldShowNotFoundViewLink = true, listEmptyContentSubtitle, footerContent = null, + headerContent = null, allApprovers, shouldShowListEmptyContent: shouldShowListEmptyContentProp = true, allowMultipleSelection = false, @@ -162,6 +164,7 @@ function ApproverSelectionList({ initiallyFocusedItemKey={initiallyFocusedOptionKey} shouldShowTextInput={shouldShowTextInput} shouldShowLoadingPlaceholder={shouldShowLoadingPlaceholder} + customListHeaderContent={headerContent} footerContent={footerContent} addBottomSafeAreaPadding shouldUpdateFocusedIndex={shouldUpdateFocusedIndex} diff --git a/src/languages/en.ts b/src/languages/en.ts index 627b54da6d28..003823604755 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2734,6 +2734,8 @@ const translations = { genericErrorMessage: "The approver couldn't be changed. Please try again or contact support.", title: 'Set approver', description: 'This person will approve the expenses.', + createNewAgent: 'Create new agent', + createNewAgentDescription: 'Automate your approvals with prompt', }, workflowsApprovalLimitPage: { title: 'Approver', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2480d37e0d66..d27d58ac260a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2549,6 +2549,8 @@ ${amount} para ${merchant} - ${date}`, genericErrorMessage: 'El aprobador no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.', title: 'Establecer aprobador', description: 'Esta persona aprobará los gastos.', + createNewAgent: 'Crear nuevo agente', + createNewAgentDescription: 'Automatiza tus aprobaciones con un prompt', }, workflowsApprovalLimitPage: { title: 'Aprobador', diff --git a/src/pages/settings/Agents/AddAgentPage.tsx b/src/pages/settings/Agents/AddAgentPage.tsx index 1ff1f40b9697..7968d3246466 100644 --- a/src/pages/settings/Agents/AddAgentPage.tsx +++ b/src/pages/settings/Agents/AddAgentPage.tsx @@ -14,6 +14,8 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePersonalDetailsByEmail from '@hooks/usePersonalDetailsByEmail'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {isMobile} from '@libs/Browser'; @@ -23,10 +25,11 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {SettingsNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import type {AvatarSource} from '@libs/UserAvatarUtils'; import {createAgent} from '@userActions/Agent'; +import {setApprovalWorkflowApprover} 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 SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/AddAgentForm'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {clearPendingAvatar, getPendingAvatar, setInitialPresetID, setNavigationToken, setReturnRoute} from './pendingAgentAvatarStore'; @@ -38,7 +41,11 @@ type AddAgentPageProps = function AddAgentPage({route}: AddAgentPageProps) { const policyID = route.params?.policyID; const workflowApproverEmail = route.params?.workflowApproverEmail; - const isWorkflowSeedFlow = !!policyID && !!workflowApproverEmail; + const isWorkflowSeedFlow = !!policyID && !!workflowApproverEmail && route.name === SCREENS.WORKSPACE.WORKFLOWS_ADD_AGENT; + const isSetApproverSeedFlow = !!policyID && !!workflowApproverEmail && route.name === SCREENS.SETTINGS.AGENTS.ADD; + const [approvalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const personalDetailsByEmail = usePersonalDetailsByEmail(); const {translate} = useLocalize(); const styles = useThemeStyles(); const {windowWidth, windowHeight} = useWindowDimensions(); @@ -76,7 +83,12 @@ function AddAgentPage({route}: AddAgentPageProps) { const presetID = botAvatarIDs.get(avatarSource as BotAvatar); setInitialPresetID(presetID); setNavigationToken(); - setReturnRoute(isWorkflowSeedFlow ? ROUTES.WORKSPACE_WORKFLOWS_ADD_AGENT.getRoute({policyID, workflowApproverEmail}) : ROUTES.SETTINGS_AGENTS_ADD.getRoute()); + const returnRoute = isWorkflowSeedFlow + ? ROUTES.WORKSPACE_WORKFLOWS_ADD_AGENT.getRoute({policyID, workflowApproverEmail}) + : isSetApproverSeedFlow + ? ROUTES.SETTINGS_AGENTS_ADD.getRoute({policyID, workflowApproverEmail}) + : ROUTES.SETTINGS_AGENTS_ADD.getRoute(); + setReturnRoute(returnRoute); Navigation.navigate(ROUTES.SETTINGS_AGENTS_ADD_AVATAR); }; @@ -96,7 +108,7 @@ function AddAgentPage({route}: AddAgentPageProps) { // Pure optimistic flow — no waiting on the server, online or offline. `createAgent` // returns the optimistic accountID it wrote into Onyx so we can hand it to the next // screen and let it render the agent with opacity until CREATE_AGENT resolves. - const {optimisticAccountID} = pendingFile + const {optimisticAccountID, avatarURI} = pendingFile ? createAgent(firstName, prompt, undefined, pendingFile.file, pendingFile.uri, policyID) : createAgent(firstName, prompt, botAvatarIDs.get(avatarSource as BotAvatar), undefined, undefined, policyID); @@ -110,6 +122,30 @@ function AddAgentPage({route}: AddAgentPageProps) { return; } + if (isSetApproverSeedFlow && policyID && approvalWorkflow) { + // Seeded from the Set Approver page: write the optimistic agent into the in-progress + // approval workflow as approver[0] with `pendingAction = ADD`. The picker's + // reconciliation effect upgrades the row to the real email/accountID once + // CREATE_AGENT lands and the agent shows up in `policy.employeeList`. + setApprovalWorkflowApprover({ + approver: { + email: '', + accountID: optimisticAccountID, + avatar: avatarURI, + displayName: firstName ?? '', + approvalLimit: null, + overLimitForwardsTo: '', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + approverIndex: 0, + currentApprovalWorkflow: approvalWorkflow, + policy, + personalDetailsByEmail, + }); + Navigation.goBack(); + return; + } + Navigation.goBack(); }; diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index bd2a1ad33b74..cbfd57da3597 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -1,16 +1,18 @@ import {useNavigationState} from '@react-navigation/native'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SelectionListApprover} from '@components/ApproverSelectionList'; import ApproverSelectionList from '@components/ApproverSelectionList'; +import MenuItem from '@components/MenuItem'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import usePersonalDetailsByEmail from '@hooks/usePersonalDetailsByEmail'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearApprovalWorkflowApprover, clearApprovalWorkflowApprovers, setApprovalWorkflowApprover} from '@libs/actions/Workflow'; import {isAnyHRReadOnlyWorkflowMode} from '@libs/HRUtils'; +import {clearApprovalWorkflowApprover, clearApprovalWorkflowApprovers, setApprovalWorkflow, setApprovalWorkflowApprover} from '@libs/actions/Workflow'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; @@ -19,6 +21,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import MemberRightIcon from '@pages/workspace/MemberRightIcon'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -31,8 +34,11 @@ type WorkspaceWorkflowsApprovalsApproverPageProps = WithPolicyAndFullscreenLoadi function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApproverPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar', 'Bot']); + const {isBetaEnabled} = usePermissions(); + const isCustomAgentEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); const [approvalWorkflow, approvalWorkflowMetadata] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); + const [optimisticAgentAccountIDMapping] = useOnyx(ONYXKEYS.OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING); const isApprovalWorkflowLoading = isLoadingOnyxValue(approvalWorkflowMetadata); const personalDetailsByEmail = usePersonalDetailsByEmail(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -221,6 +227,70 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa [translate, styles.textHeadlineH1, styles.mh5, styles.mv3, styles.mb3, styles.textSupporting], ); + const shouldShowCreateAgentRow = isCustomAgentEnabled && !isChangeApproverRoute && approverIndex === 0; + + const onCreateAgentPress = useCallback(() => { + Navigation.navigate( + ROUTES.SETTINGS_AGENTS_ADD.getRoute({ + policyID: route.params.policyID, + workflowApproverEmail: firstApprover, + }), + ); + }, [route.params.policyID, firstApprover]); + + const headerContent = useMemo(() => { + if (!shouldShowCreateAgentRow) { + return null; + } + return ( + + ); + }, [shouldShowCreateAgentRow, icons.Bot, translate, onCreateAgentPress]); + + // Reconcile the optimistic agent approver once the server-side CREATE_AGENT response lands. + // The new agent is written to `approvalWorkflow.approvers[approverIndex]` with `accountID` set + // but `email = ''` and `pendingAction = ADD`. When the server replies, the real accountID + // (mapped via `OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING`) shows up in `employeeList` with a login; + // upgrade the approver to the real email so the picker can match it to a row in `allApprovers`. + useEffect(() => { + if (!approvalWorkflow || !policy?.employeeList || !personalDetails) { + return; + } + const pendingApprover = approvalWorkflow.approvers[approverIndex]; + if (!pendingApprover || pendingApprover.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || pendingApprover.email || !pendingApprover.accountID) { + return; + } + const mappedRealAccountID = optimisticAgentAccountIDMapping?.[pendingApprover.accountID]; + if (!mappedRealAccountID) { + return; + } + const mappedDetail = personalDetails[mappedRealAccountID]; + if (!mappedDetail?.login || !policy.employeeList?.[mappedDetail.login]) { + return; + } + const upgradedApprovers = approvalWorkflow.approvers.map((approver, index) => + index === approverIndex && approver + ? { + ...approver, + email: mappedDetail.login as string, + accountID: mappedDetail.accountID, + avatar: mappedDetail.avatar ?? approver.avatar, + displayName: mappedDetail.displayName ?? approver.displayName, + pendingAction: undefined, + } + : approver, + ); + setApprovalWorkflow({...approvalWorkflow, approvers: upgradedApprovers}); + }, [approvalWorkflow, policy?.employeeList, personalDetails, optimisticAgentAccountIDMapping, approverIndex]); + return ( Date: Thu, 28 May 2026 12:16:46 -0500 Subject: [PATCH 2/8] feat: add agent support and enhance member icon display - Introduced a new translation for 'Agent' in the English language file. - Updated MemberRightIcon component to include an `isAgent` prop for displaying agent badges. - Enhanced WorkspaceWorkflowsApprovalsApproverPage to conditionally render agent-related information and manage optimistic agent states. - Added styles for agent approver avatars to improve UI consistency. --- src/languages/en.ts | 1 + src/pages/workspace/MemberRightIcon.tsx | 5 ++++- .../WorkspaceWorkflowsApprovalsApproverPage.tsx | 13 ++++++++----- src/styles/index.ts | 7 +++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 003823604755..6e06dda9c8de 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -359,6 +359,7 @@ const translations = { update: 'Update', member: 'Member', auditor: 'Auditor', + agent: 'Agent', role: 'Role', roleCannotBeChanged: (workflowsLinkPage: string) => `Role can't be changed because this member is a payer on this workspace.`, currency: 'Currency', diff --git a/src/pages/workspace/MemberRightIcon.tsx b/src/pages/workspace/MemberRightIcon.tsx index 647161c3b587..6373eaebe929 100644 --- a/src/pages/workspace/MemberRightIcon.tsx +++ b/src/pages/workspace/MemberRightIcon.tsx @@ -11,10 +11,11 @@ type MemberRightIconProps = { owner?: string; role?: string; login?: string; + isAgent?: boolean; badgeStyles?: StyleProp; }; -export default function MemberRightIcon({role, owner, login, badgeStyles}: MemberRightIconProps) { +export default function MemberRightIcon({role, owner, login, isAgent = false, badgeStyles}: MemberRightIconProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isFocused} = useListItemFocus(); @@ -26,6 +27,8 @@ export default function MemberRightIcon({role, owner, login, badgeStyles}: Membe badgeText = 'common.admin'; } else if (role === CONST.POLICY.ROLE.AUDITOR) { badgeText = 'common.auditor'; + } else if (isAgent) { + badgeText = 'common.agent'; } if (badgeText) { return ( diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index cbfd57da3597..ada076584b59 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -11,7 +11,7 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePersonalDetailsByEmail from '@hooks/usePersonalDetailsByEmail'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isAnyHRReadOnlyWorkflowMode} from '@libs/HRUtils'; +import colors from '@styles/theme/colors'; import {clearApprovalWorkflowApprover, clearApprovalWorkflowApprovers, setApprovalWorkflow, setApprovalWorkflowApprover} from '@libs/actions/Workflow'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -123,6 +123,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa role={employee.role} owner={policy?.owner} login={login} + isAgent={isAgentEmail(employee.email)} /> ), }; @@ -227,7 +228,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa [translate, styles.textHeadlineH1, styles.mh5, styles.mv3, styles.mb3, styles.textSupporting], ); - const shouldShowCreateAgentRow = isCustomAgentEnabled && !isChangeApproverRoute && approverIndex === 0; + const shouldShowCreateAgentRow = isCustomAgentEnabled && approverIndex === 0; const onCreateAgentPress = useCallback(() => { Navigation.navigate( @@ -245,15 +246,17 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa return ( ); - }, [shouldShowCreateAgentRow, icons.Bot, translate, onCreateAgentPress]); + }, [shouldShowCreateAgentRow, icons.Bot, translate, onCreateAgentPress, styles.avatarAgentApprover]); // Reconcile the optimistic agent approver once the server-side CREATE_AGENT response lands. // The new agent is written to `approvalWorkflow.approvers[approverIndex]` with `accountID` set diff --git a/src/styles/index.ts b/src/styles/index.ts index 18dfe5c44650..6a9aa0b3858a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5469,6 +5469,13 @@ const staticStyles = (theme: ThemeColors) => borderRadius: variables.componentBorderRadiusNormal, }, + avatarAgentApprover: { + backgroundColor: theme.icon, + borderRadius: variables.componentBorderRadiusCircle, + height: 40, + width: 40, + }, + successBankSharedCardIllustration: { width: 164, height: 164, From 94ca0760b8179b9fecabaaeca1a456f5a035d2eb Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Thu, 28 May 2026 12:27:05 -0500 Subject: [PATCH 3/8] feat: enhance approval workflow with optimistic agent handling - Added `onDismissError` prop to ApproverSelectionList for improved error management. - Implemented `clearOptimisticAgentFromApprovalWorkflow` function to handle optimistic agent state during approval processes. - Updated AddAgentPage to navigate correctly based on approval workflow actions. - Enhanced WorkspaceWorkflowsApprovalsApproverPage to manage optimistic approver display and dismissal, improving user experience in agent management. --- src/components/ApproverSelectionList.tsx | 3 + src/libs/actions/Agent.ts | 17 +++++ src/pages/settings/Agents/AddAgentPage.tsx | 10 ++- ...orkspaceWorkflowsApprovalsApproverPage.tsx | 70 ++++++++++++++++++- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/components/ApproverSelectionList.tsx b/src/components/ApproverSelectionList.tsx index 40023ab96a1f..8b4c7227daa4 100644 --- a/src/components/ApproverSelectionList.tsx +++ b/src/components/ApproverSelectionList.tsx @@ -39,6 +39,7 @@ type ApproverSelectionListPageProps = { shouldShowListEmptyContent?: boolean; allowMultipleSelection?: boolean; onSelectApprover?: (approvers: SelectionListApprover[]) => void; + onDismissError?: (approver: SelectionListApprover) => void; shouldShowLoadingPlaceholder?: boolean; shouldEnableHeaderMaxHeight?: boolean; shouldUpdateFocusedIndex?: boolean; @@ -66,6 +67,7 @@ function ApproverSelectionList({ shouldShowListEmptyContent: shouldShowListEmptyContentProp = true, allowMultipleSelection = false, onSelectApprover, + onDismissError, shouldShowLoadingPlaceholder, shouldEnableHeaderMaxHeight, shouldUpdateFocusedIndex = true, @@ -155,6 +157,7 @@ function ApproverSelectionList({ real ID mapping slot, and the policy-level + * addAgent error. Used by the RBR X click on the Set Approver page. + */ +function clearOptimisticAgentFromApprovalWorkflow(policyID: string, approverIndex: number, currentApprovalWorkflow: ApprovalWorkflowOnyx | undefined, optimisticAccountID: number) { + clearApprovalWorkflowApprover({approverIndex, currentApprovalWorkflow}); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[optimisticAccountID]: null}); + Onyx.set(`${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${optimisticAccountID}`, null); + Onyx.merge(ONYXKEYS.OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING, {[optimisticAccountID]: null}); + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {[CONST.POLICY.COLLECTION_KEYS.ADD_AGENT]: null}}); +} + function clearAgentUpdateError(accountID: number) { Onyx.merge(`${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${accountID}`, {errors: null, nameErrors: null, promptErrors: null, avatarErrors: null}); } @@ -358,6 +374,7 @@ export { createAgent, clearAgentError, clearPendingAgentFromApprovalWorkflow, + clearOptimisticAgentFromApprovalWorkflow, clearAgentUpdateError, clearAgentNameUpdateError, clearAgentPromptUpdateError, diff --git a/src/pages/settings/Agents/AddAgentPage.tsx b/src/pages/settings/Agents/AddAgentPage.tsx index 7968d3246466..cc81a25b7fed 100644 --- a/src/pages/settings/Agents/AddAgentPage.tsx +++ b/src/pages/settings/Agents/AddAgentPage.tsx @@ -142,7 +142,15 @@ function AddAgentPage({route}: AddAgentPageProps) { policy, personalDetailsByEmail, }); - Navigation.goBack(); + // Pop AddAgentPage + the Set Approver picker so the admin lands on the workflow + // edit/create screen with the optimistic agent already chosen as approver[0]. From + // there the Save button commits the workflow (the agent renders opaque until + // CREATE_AGENT resolves; on failure the picker's RBR X clears the pending agent). + if (approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.EDIT && workflowApproverEmail) { + Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, workflowApproverEmail)); + } else { + Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)); + } return; } diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index ada076584b59..940b5aafb7d6 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -11,16 +11,19 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePersonalDetailsByEmail from '@hooks/usePersonalDetailsByEmail'; import useThemeStyles from '@hooks/useThemeStyles'; -import colors from '@styles/theme/colors'; +import {clearOptimisticAgentFromApprovalWorkflow} from '@libs/actions/Agent'; import {clearApprovalWorkflowApprover, clearApprovalWorkflowApprovers, setApprovalWorkflow, setApprovalWorkflowApprover} from '@libs/actions/Workflow'; +import {isAnyHRReadOnlyWorkflowMode} from '@libs/HRUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import {getDefaultApprover, getMemberAccountIDsForWorkspace, isExpensifyTeam, shouldFilterExpensifyTeam} from '@libs/PolicyUtils'; +import {isAgentEmail} from '@libs/SessionUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import MemberRightIcon from '@pages/workspace/MemberRightIcon'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -39,6 +42,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa const isCustomAgentEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); const [approvalWorkflow, approvalWorkflowMetadata] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW); const [optimisticAgentAccountIDMapping] = useOnyx(ONYXKEYS.OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING); + const [agentPrompts] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT); const isApprovalWorkflowLoading = isLoadingOnyxValue(approvalWorkflowMetadata); const personalDetailsByEmail = usePersonalDetailsByEmail(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -145,6 +149,65 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa shouldFilterOutExpensifyTeam, ]); + // Optimistic agent approver (seeded by AddAgentPage). The pending approver carries the + // optimistic accountID but no email until CREATE_AGENT resolves; render it as a top row + // with reduced opacity (via `pendingAction`) and surface CREATE_AGENT errors so the admin + // can dismiss + retry without leaving the picker. + const pendingOptimisticApprover = + currentApprover?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !!currentApprover?.accountID && !currentApprover.email ? currentApprover : undefined; + const pendingOptimisticAccountID = pendingOptimisticApprover?.accountID; + const pendingOptimisticDetail = pendingOptimisticAccountID ? personalDetails?.[pendingOptimisticAccountID] : undefined; + const pendingOptimisticPromptErrors = pendingOptimisticAccountID ? agentPrompts?.[`${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${pendingOptimisticAccountID}`]?.errors : undefined; + const addAgentPolicyErrors = policy?.errorFields?.[CONST.POLICY.COLLECTION_KEYS.ADD_AGENT]; + const pendingOptimisticErrors = addAgentPolicyErrors ?? pendingOptimisticPromptErrors; + + const pendingOptimisticDetailDisplayName = pendingOptimisticDetail?.displayName; + const pendingOptimisticDetailAvatar = pendingOptimisticDetail?.avatar; + const pendingOptimisticApproverRow = useMemo(() => { + if (!pendingOptimisticApprover || !pendingOptimisticAccountID) { + return undefined; + } + const displayName = pendingOptimisticDetailDisplayName ?? pendingOptimisticApprover.displayName ?? ''; + const avatar = pendingOptimisticDetailAvatar ?? pendingOptimisticApprover.avatar; + return { + text: displayName, + keyForList: String(pendingOptimisticAccountID), + isSelected: true, + login: '', + icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: pendingOptimisticAccountID}], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + errors: pendingOptimisticErrors ?? undefined, + isInteractive: false, + }; + }, [pendingOptimisticApprover, pendingOptimisticAccountID, pendingOptimisticDetailDisplayName, pendingOptimisticDetailAvatar, pendingOptimisticErrors, icons.FallbackAvatar]); + + // Once CREATE_AGENT resolves, the server echoes `{optimisticID: realID}` in + // `OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING` and the real agent lands in `policy.employeeList`, + // which means `allApprovers` already contains the real row before the reconcile effect + // rewrites `APPROVAL_WORKFLOW`. In that interim render we'd otherwise show both rows. + // Detect the mapping early, force the real row to render as selected, and drop the + // optimistic placeholder. + const reconciledRealAccountID = pendingOptimisticAccountID ? optimisticAgentAccountIDMapping?.[pendingOptimisticAccountID] : undefined; + const reconciledLogin = reconciledRealAccountID ? personalDetails?.[reconciledRealAccountID]?.login : undefined; + const isOptimisticReconciled = !!reconciledLogin && !!policy?.employeeList?.[reconciledLogin]; + + const allApproversWithOptimistic = useMemo(() => { + if (isOptimisticReconciled && reconciledLogin) { + return allApprovers.map((approver) => (approver.keyForList === reconciledLogin ? {...approver, isSelected: true} : approver)); + } + if (pendingOptimisticApproverRow) { + return [pendingOptimisticApproverRow, ...allApprovers]; + } + return allApprovers; + }, [pendingOptimisticApproverRow, allApprovers, isOptimisticReconciled, reconciledLogin]); + + const onDismissOptimisticApprover = useCallback(() => { + if (!pendingOptimisticAccountID) { + return; + } + clearOptimisticAgentFromApprovalWorkflow(route.params.policyID, approverIndex, approvalWorkflow, pendingOptimisticAccountID); + }, [pendingOptimisticAccountID, route.params.policyID, approverIndex, approvalWorkflow]); + const shouldShowListEmptyContent = !!approvalWorkflow && !isApprovalWorkflowLoading && !removingApproverEmail; const goBack = useCallback(() => { @@ -306,15 +369,16 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa headerContent={headerContent} isLoadingReportData={isLoadingReportData} policy={policy} - initiallyFocusedOptionKey={visibleSelectedApproverEmail} + initiallyFocusedOptionKey={(isOptimisticReconciled ? reconciledLogin : pendingOptimisticApproverRow?.keyForList) ?? visibleSelectedApproverEmail} shouldShowNotFoundView={shouldShowNotFoundView} shouldShowNotFoundViewLink - allApprovers={allApprovers} + allApprovers={allApproversWithOptimistic} onBackButtonPress={goBack} shouldShowListEmptyContent={shouldShowListEmptyContent} listEmptyContentSubtitle={translate('workflowsPage.emptyContent.approverSubtitle')} allowMultipleSelection={false} onSelectApprover={toggleApprover} + onDismissError={onDismissOptimisticApprover} /> ); From bdaaf14aeddd97ea71aaa0a7255486207f3b9568 Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Fri, 29 May 2026 14:18:28 -0500 Subject: [PATCH 4/8] fix: translations and CurrentUser arg in useAddAgentToApprovalWorkflow Co-Authored-By: Claude Opus 4.7 (1M context) --- src/languages/de.ts | 3 +++ src/languages/es.ts | 3 ++- src/languages/fr.ts | 5 ++++- src/languages/it.ts | 3 +++ src/languages/ja.ts | 5 ++++- src/languages/nl.ts | 3 +++ src/languages/pl.ts | 3 +++ src/languages/pt-BR.ts | 3 +++ src/languages/zh-hans.ts | 3 +++ 9 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 73d20ca03d4a..11c1fd71cfbf 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -497,6 +497,7 @@ const translations: TranslationDeepObject = { previousYear: 'Vorheriges Jahr', nextYear: 'Nächstes Jahr', avatar: 'Avatar', + agent: 'Agent', }, socials: { podcast: 'Folgen Sie uns auf Podcast', @@ -2674,6 +2675,8 @@ ${amount} für ${merchant} – ${date}`, genericErrorMessage: 'Die genehmigende Person konnte nicht geändert werden. Bitte versuche es erneut oder kontaktiere den Support.', title: 'Genehmigenden festlegen', description: 'Diese Person wird die Ausgaben genehmigen.', + createNewAgent: 'Neue Agent:in erstellen', + createNewAgentDescription: 'Automatisieren Sie Ihre Genehmigungen mit Prompt', }, workflowsApprovalLimitPage: { title: 'Genehmiger', diff --git a/src/languages/es.ts b/src/languages/es.ts index d27d58ac260a..26e589906e15 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -449,6 +449,7 @@ const translations: TranslationDeepObject = { expensifyLogo: 'Logo de Expensify', approver: 'Aprobador', enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `introducir dígito ${digitIndex} de ${totalDigits}`, + agent: 'Agente', }, socials: { podcast: 'Síguenos en Podcast', @@ -2550,7 +2551,7 @@ ${amount} para ${merchant} - ${date}`, title: 'Establecer aprobador', description: 'Esta persona aprobará los gastos.', createNewAgent: 'Crear nuevo agente', - createNewAgentDescription: 'Automatiza tus aprobaciones con un prompt', + createNewAgentDescription: 'Automatiza tus aprobaciones con rapidez', }, workflowsApprovalLimitPage: { title: 'Aprobador', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index ead01bebcc4e..01fe8f7ed319 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -497,6 +497,7 @@ const translations: TranslationDeepObject = { previousYear: 'Année précédente', nextYear: 'L’an prochain', avatar: 'Avatar', + agent: 'Agent', }, socials: { podcast: 'Suivez-nous sur Podcast', @@ -2681,6 +2682,8 @@ ${amount} pour ${merchant} - ${date}`, genericErrorMessage: 'L’approbateur n’a pas pu être modifié. Veuillez réessayer ou contacter l’assistance.', title: 'Définir l’approbateur', description: 'Cette personne approuvera les dépenses.', + createNewAgent: 'Créer un nouvel agent', + createNewAgentDescription: 'Automatisez vos approbations avec l’IA', }, workflowsApprovalLimitPage: { title: 'Approbateur', @@ -2978,7 +2981,7 @@ ${amount} pour ${merchant} - ${date}`, phoneOrEmail: 'Téléphone ou e-mail', error: { agentSignInBlocked: - 'Les comptes d\u2019agent ne permettent pas de se connecter directement. Pour utiliser un agent, connectez-vous avec votre propre compte et accédez-y via Copilot.', + 'Les comptes d’agent ne permettent pas de se connecter directement. Pour utiliser un agent, connectez-vous avec votre propre compte et accédez-y via Copilot.', invalidFormatEmailLogin: 'L’adresse e-mail saisie est invalide. Veuillez corriger le format et réessayer.', }, cannotGetAccountDetails: 'Impossible de récupérer les détails du compte. Veuillez essayer de vous reconnecter.', diff --git a/src/languages/it.ts b/src/languages/it.ts index e358d2cf3e85..1b2effccc77d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -497,6 +497,7 @@ const translations: TranslationDeepObject = { previousYear: 'Anno precedente', nextYear: "L'anno prossimo", avatar: 'Avatar', + agent: 'Agente', }, socials: { podcast: 'Seguici su Podcast', @@ -2669,6 +2670,8 @@ ${amount} per ${merchant} - ${date}`, genericErrorMessage: "Non è stato possibile modificare l'approvatore. Riprova o contatta l'assistenza.", title: 'Imposta approvatore', description: 'Questa persona approverà le spese.', + createNewAgent: 'Crea nuovo agente', + createNewAgentDescription: 'Automatizza le tue approvazioni con prompt', }, workflowsApprovalLimitPage: { title: 'Approvante', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 340cdace1a8d..95be8740f19f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -496,6 +496,7 @@ const translations: TranslationDeepObject = { previousYear: '前年', nextYear: '来年', avatar: 'アバター', + agent: 'エージェント', }, socials: { podcast: 'ポッドキャストでフォロー', @@ -2560,7 +2561,7 @@ ${date} の ${merchant} への ${amount}`, accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `${members} の経費で、承認者は ${approvers} です`, addApprovalButton: '承認ワークフローを追加', editWorkflowAction: '編集', - addAgentAction: 'エージェントを追加', + addAgentAction: '担当者を追加', findWorkflow: 'ワークフローを検索', addApprovalTip: 'より詳細なワークフローが存在する場合を除き、このデフォルトのワークフローがすべてのメンバーに適用されます。', approver: '承認者', @@ -2647,6 +2648,8 @@ ${date} の ${merchant} への ${amount}`, genericErrorMessage: '承認者を変更できませんでした。もう一度お試しいただくか、サポートにお問い合わせください。', title: '承認者を設定', description: 'この人が経費を承認します。', + createNewAgent: '新しいエージェントを作成', + createNewAgentDescription: '迅速な承認でワークフローを自動化しましょう', }, workflowsApprovalLimitPage: { title: '承認者', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5c4c429c12c2..d5890f3a7159 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -496,6 +496,7 @@ const translations: TranslationDeepObject = { previousYear: 'Vorig jaar', nextYear: 'Volgend jaar', avatar: 'Avatar', + agent: 'Agent', }, socials: { podcast: 'Volg ons op Podcast', @@ -2666,6 +2667,8 @@ ${amount} voor ${merchant} - ${date}`, genericErrorMessage: 'De fiatteur kon niet worden gewijzigd. Probeer het opnieuw of neem contact op met support.', title: 'Stel fiatteur in', description: 'Deze persoon keurt de declaraties goed.', + createNewAgent: 'Nieuwe agent toevoegen', + createNewAgentDescription: 'Automatiseer je goedkeuringen met prompt', }, workflowsApprovalLimitPage: { title: 'Fiatteur', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 66a543121ff2..ec80df214a81 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -496,6 +496,7 @@ const translations: TranslationDeepObject = { previousYear: 'Poprzedni rok', nextYear: 'W przyszłym roku', avatar: 'Avatar', + agent: 'Agent', }, socials: { podcast: 'Śledź nas na Podcast', @@ -2662,6 +2663,8 @@ ${amount} dla ${merchant} - ${date}`, genericErrorMessage: 'Nie udało się zmienić osoby zatwierdzającej. Spróbuj ponownie lub skontaktuj się z pomocą techniczną.', title: 'Ustaw zatwierdzającego', description: 'Ta osoba będzie zatwierdzać wydatki.', + createNewAgent: 'Utwórz nowego agenta', + createNewAgentDescription: 'Automatyzuj swoje akceptacje za pomocą promptów', }, workflowsApprovalLimitPage: { title: 'Osoba zatwierdzająca', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5155a7433031..eb4fce649435 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -495,6 +495,7 @@ const translations: TranslationDeepObject = { previousYear: 'Ano anterior', nextYear: 'Ano que vem', avatar: 'Avatar', + agent: 'Agente', }, socials: { podcast: 'Siga-nos no Podcast', @@ -2660,6 +2661,8 @@ ${amount} para ${merchant} - ${date}`, genericErrorMessage: 'O aprovador não pôde ser alterado. Tente novamente ou entre em contato com o suporte.', title: 'Definir aprovador', description: 'Essa pessoa vai aprovar as despesas.', + createNewAgent: 'Criar novo agente', + createNewAgentDescription: 'Automatize suas aprovações com agilidade', }, workflowsApprovalLimitPage: { title: 'Aprovador', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index e77148a15411..acde03798035 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -492,6 +492,7 @@ const translations: TranslationDeepObject = { previousYear: '上一年', nextYear: '明年', avatar: '头像', + agent: '代理人', }, socials: { podcast: '在播客上关注我们', @@ -2594,6 +2595,8 @@ ${amount},商户:${merchant} - 日期:${date}`, genericErrorMessage: '无法更改审批人。请重试或联系支持。', title: '设置审批人', description: '此人将审核并批准这些报销。', + createNewAgent: '创建新代理', + createNewAgentDescription: '使用 Prompt 自动化您的审批', }, workflowsApprovalLimitPage: { title: '审批人', From d7a22aeb1fc344bc91920ce44533dc6cabd31056 Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Fri, 29 May 2026 14:51:41 -0500 Subject: [PATCH 5/8] refactor: update AddAgentPage test to improve route handling - Changed the `makeRoute` function to accept an optional `name` parameter for better flexibility in route creation. - Updated the test case for `AddAgentPage` to use the new route handling, ensuring accurate navigation based on the approval workflow. --- tests/unit/pages/settings/AddAgentPageTest.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/pages/settings/AddAgentPageTest.tsx b/tests/unit/pages/settings/AddAgentPageTest.tsx index 976a43f88352..ecabeadbf2a3 100644 --- a/tests/unit/pages/settings/AddAgentPageTest.tsx +++ b/tests/unit/pages/settings/AddAgentPageTest.tsx @@ -7,7 +7,7 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AddAgentPage from '@pages/settings/Agents/AddAgentPage'; import {setInitialPresetID, setNavigationToken} from '@pages/settings/Agents/pendingAgentAvatarStore'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; jest.mock('@userActions/Agent', () => ({ createAgent: jest.fn(() => ({optimisticAccountID: -123456, avatarURI: undefined})), @@ -114,8 +114,8 @@ const mockUseCurrentUserPersonalDetails = jest.mocked(useCurrentUserPersonalDeta type AddAgentRouteProp = PlatformStackRouteProp; -function makeRoute(params: AddAgentRouteProp['params'] = {}): AddAgentRouteProp { - return {name: '', key: '', params} as unknown as AddAgentRouteProp; +function makeRoute(params: AddAgentRouteProp['params'] = {}, name = ''): AddAgentRouteProp { + return {name, key: '', params} as unknown as AddAgentRouteProp; } describe('AddAgentPage', () => { @@ -252,7 +252,7 @@ describe('AddAgentPage', () => { // response lands. render( , ); From 6ecc393bad996a7e15d26f3810e72ae3d1b566b6 Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Fri, 29 May 2026 15:09:16 -0500 Subject: [PATCH 6/8] refactor: improve route handling in AddAgentPage and optimize approver retrieval in WorkspaceWorkflowsApprovalsApproverPage - Refactored route determination logic in AddAgentPage for better readability and maintainability. - Updated the method of accessing approvers in WorkspaceWorkflowsApprovalsApproverPage to use the `at` method for improved clarity and performance. --- src/pages/settings/Agents/AddAgentPage.tsx | 13 ++++++++----- .../WorkspaceWorkflowsApprovalsApproverPage.tsx | 7 ++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Agents/AddAgentPage.tsx b/src/pages/settings/Agents/AddAgentPage.tsx index cc81a25b7fed..95eb8c3e7d5a 100644 --- a/src/pages/settings/Agents/AddAgentPage.tsx +++ b/src/pages/settings/Agents/AddAgentPage.tsx @@ -83,11 +83,14 @@ function AddAgentPage({route}: AddAgentPageProps) { const presetID = botAvatarIDs.get(avatarSource as BotAvatar); setInitialPresetID(presetID); setNavigationToken(); - const returnRoute = isWorkflowSeedFlow - ? ROUTES.WORKSPACE_WORKFLOWS_ADD_AGENT.getRoute({policyID, workflowApproverEmail}) - : isSetApproverSeedFlow - ? ROUTES.SETTINGS_AGENTS_ADD.getRoute({policyID, workflowApproverEmail}) - : ROUTES.SETTINGS_AGENTS_ADD.getRoute(); + let returnRoute; + if (isWorkflowSeedFlow) { + returnRoute = ROUTES.WORKSPACE_WORKFLOWS_ADD_AGENT.getRoute({policyID, workflowApproverEmail}); + } else if (isSetApproverSeedFlow) { + returnRoute = ROUTES.SETTINGS_AGENTS_ADD.getRoute({policyID, workflowApproverEmail}); + } else { + returnRoute = ROUTES.SETTINGS_AGENTS_ADD.getRoute(); + } setReturnRoute(returnRoute); Navigation.navigate(ROUTES.SETTINGS_AGENTS_ADD_AVATAR); }; diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index 940b5aafb7d6..d8272e53d6ca 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -330,7 +330,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa if (!approvalWorkflow || !policy?.employeeList || !personalDetails) { return; } - const pendingApprover = approvalWorkflow.approvers[approverIndex]; + const pendingApprover = approvalWorkflow.approvers.at(approverIndex); if (!pendingApprover || pendingApprover.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || pendingApprover.email || !pendingApprover.accountID) { return; } @@ -339,14 +339,15 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa return; } const mappedDetail = personalDetails[mappedRealAccountID]; - if (!mappedDetail?.login || !policy.employeeList?.[mappedDetail.login]) { + const mappedLogin = mappedDetail?.login; + if (!mappedLogin || !policy.employeeList?.[mappedLogin]) { return; } const upgradedApprovers = approvalWorkflow.approvers.map((approver, index) => index === approverIndex && approver ? { ...approver, - email: mappedDetail.login as string, + email: mappedLogin, accountID: mappedDetail.accountID, avatar: mappedDetail.avatar ?? approver.avatar, displayName: mappedDetail.displayName ?? approver.displayName, From c4b42ab72f7411910aceb2cfa3bc12133171a38d Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Fri, 29 May 2026 17:30:27 -0500 Subject: [PATCH 7/8] fix: removing unwanted use calls due to react compiler --- ...orkspaceWorkflowsApprovalsApproverPage.tsx | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index d8272e53d6ca..2723eab72a36 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -163,7 +163,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa const pendingOptimisticDetailDisplayName = pendingOptimisticDetail?.displayName; const pendingOptimisticDetailAvatar = pendingOptimisticDetail?.avatar; - const pendingOptimisticApproverRow = useMemo(() => { + const pendingOptimisticApproverRow: SelectionListApprover | undefined = (() => { if (!pendingOptimisticApprover || !pendingOptimisticAccountID) { return undefined; } @@ -179,7 +179,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa errors: pendingOptimisticErrors ?? undefined, isInteractive: false, }; - }, [pendingOptimisticApprover, pendingOptimisticAccountID, pendingOptimisticDetailDisplayName, pendingOptimisticDetailAvatar, pendingOptimisticErrors, icons.FallbackAvatar]); + })(); // Once CREATE_AGENT resolves, the server echoes `{optimisticID: realID}` in // `OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING` and the real agent lands in `policy.employeeList`, @@ -191,7 +191,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa const reconciledLogin = reconciledRealAccountID ? personalDetails?.[reconciledRealAccountID]?.login : undefined; const isOptimisticReconciled = !!reconciledLogin && !!policy?.employeeList?.[reconciledLogin]; - const allApproversWithOptimistic = useMemo(() => { + const allApproversWithOptimistic = (() => { if (isOptimisticReconciled && reconciledLogin) { return allApprovers.map((approver) => (approver.keyForList === reconciledLogin ? {...approver, isSelected: true} : approver)); } @@ -199,14 +199,14 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa return [pendingOptimisticApproverRow, ...allApprovers]; } return allApprovers; - }, [pendingOptimisticApproverRow, allApprovers, isOptimisticReconciled, reconciledLogin]); + })(); - const onDismissOptimisticApprover = useCallback(() => { + const onDismissOptimisticApprover = () => { if (!pendingOptimisticAccountID) { return; } clearOptimisticAgentFromApprovalWorkflow(route.params.policyID, approverIndex, approvalWorkflow, pendingOptimisticAccountID); - }, [pendingOptimisticAccountID, route.params.policyID, approverIndex, approvalWorkflow]); + }; const shouldShowListEmptyContent = !!approvalWorkflow && !isApprovalWorkflowLoading && !removingApproverEmail; @@ -293,33 +293,28 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa const shouldShowCreateAgentRow = isCustomAgentEnabled && approverIndex === 0; - const onCreateAgentPress = useCallback(() => { + const onCreateAgentPress = () => { Navigation.navigate( ROUTES.SETTINGS_AGENTS_ADD.getRoute({ policyID: route.params.policyID, workflowApproverEmail: firstApprover, }), ); - }, [route.params.policyID, firstApprover]); - - const headerContent = useMemo(() => { - if (!shouldShowCreateAgentRow) { - return null; - } - return ( - - ); - }, [shouldShowCreateAgentRow, icons.Bot, translate, onCreateAgentPress, styles.avatarAgentApprover]); + }; + + const headerContent = !shouldShowCreateAgentRow ? null : ( + + ); // Reconcile the optimistic agent approver once the server-side CREATE_AGENT response lands. // The new agent is written to `approvalWorkflow.approvers[approverIndex]` with `accountID` set From 9311ad95d376c0036b34d9b2867c35129981ccda Mon Sep 17 00:00:00 2001 From: Nicolas Bonet Date: Mon, 1 Jun 2026 13:56:14 -0500 Subject: [PATCH 8/8] Enhance agent creation workflow by refining optimistic detail handling and improving navigation logic. Added check for optimistic personal details in WorkflowUtils and updated AddAgentPage to use the original first approver email for navigation. Adjusted WorkspaceWorkflowsApprovalsApproverPage to handle selected approver email correctly. --- src/libs/WorkflowUtils.ts | 9 +++++++++ src/pages/settings/Agents/AddAgentPage.tsx | 8 ++++++-- .../WorkspaceWorkflowsApprovalsApproverPage.tsx | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 559eae99da31..a78e604bfcec 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -774,6 +774,15 @@ function resolveOptimisticAgent({ } } + // When the optimistic personal detail is still present, this tab originated the + // CREATE_AGENT write and the mapping will land here. Falling back to prompt-match in + // that window would collapse the brand-new optimistic agent into another agent that + // shares the same prompt text (e.g. two agents created with the default prompt) — + // wait for the mapping instead. + if (personalDetails[optimisticAccountID]?.isOptimisticPersonalDetail) { + return undefined; + } + if (!pendingAgentPrompt || !agentPrompts) { return undefined; } diff --git a/src/pages/settings/Agents/AddAgentPage.tsx b/src/pages/settings/Agents/AddAgentPage.tsx index 95eb8c3e7d5a..7c9ce3b339af 100644 --- a/src/pages/settings/Agents/AddAgentPage.tsx +++ b/src/pages/settings/Agents/AddAgentPage.tsx @@ -149,8 +149,12 @@ function AddAgentPage({route}: AddAgentPageProps) { // edit/create screen with the optimistic agent already chosen as approver[0]. From // there the Save button commits the workflow (the agent renders opaque until // CREATE_AGENT resolves; on failure the picker's RBR X clears the pending agent). - if (approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.EDIT && workflowApproverEmail) { - Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, workflowApproverEmail)); + // The EDIT route is keyed by the workflow's original first approver email (the saved + // state); read it from the in-memory workflow rather than the URL param, which may + // carry the picker's in-progress selection instead of the workflow identifier. + const originalFirstApproverEmail = approvalWorkflow.originalApprovers?.at(0)?.email; + if (approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.EDIT && originalFirstApproverEmail) { + Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, originalFirstApproverEmail)); } else { Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID)); } diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx index 2723eab72a36..783416c6bc04 100644 --- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage.tsx @@ -297,7 +297,7 @@ function WorkspaceWorkflowsApprovalsApproverPage({policy, personalDetails, isLoa Navigation.navigate( ROUTES.SETTINGS_AGENTS_ADD.getRoute({ policyID: route.params.policyID, - workflowApproverEmail: firstApprover, + workflowApproverEmail: selectedApproverEmail ?? firstApprover, }), ); };