Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/ApproverSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ type ApproverSelectionListPageProps = {
shouldShowNotFoundViewLink?: boolean;
listEmptyContentSubtitle?: string;
footerContent?: React.ReactNode;
headerContent?: React.JSX.Element | null;
subtitle?: React.ReactNode;
shouldShowTextInput?: boolean;
allApprovers: SelectionListApprover[];
shouldShowListEmptyContent?: boolean;
allowMultipleSelection?: boolean;
onSelectApprover?: (approvers: SelectionListApprover[]) => void;
onDismissError?: (approver: SelectionListApprover) => void;
shouldShowLoadingPlaceholder?: boolean;
shouldEnableHeaderMaxHeight?: boolean;
shouldUpdateFocusedIndex?: boolean;
Expand All @@ -60,10 +62,12 @@ function ApproverSelectionList({
shouldShowNotFoundViewLink = true,
listEmptyContentSubtitle,
footerContent = null,
headerContent = null,
allApprovers,
shouldShowListEmptyContent: shouldShowListEmptyContentProp = true,
allowMultipleSelection = false,
onSelectApprover,
onDismissError,
shouldShowLoadingPlaceholder,
shouldEnableHeaderMaxHeight,
shouldUpdateFocusedIndex = true,
Expand Down Expand Up @@ -153,6 +157,7 @@ function ApproverSelectionList({
<SelectionList
data={data}
onSelectRow={toggleApprover}
onDismissError={onDismissError}
ListItem={InviteMemberListItem}
textInputOptions={textInputOptions}
canSelectMultiple={allowMultipleSelection}
Expand All @@ -162,6 +167,7 @@ function ApproverSelectionList({
initiallyFocusedItemKey={initiallyFocusedOptionKey}
shouldShowTextInput={shouldShowTextInput}
shouldShowLoadingPlaceholder={shouldShowLoadingPlaceholder}
customListHeaderContent={headerContent}
footerContent={footerContent}
addBottomSafeAreaPadding
shouldUpdateFocusedIndex={shouldUpdateFocusedIndex}
Expand Down
3 changes: 3 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: 'Vorheriges Jahr',
nextYear: 'Nächstes Jahr',
avatar: 'Avatar',
agent: 'Agent',
restrictions: 'Beschränkungen',
},
socials: {
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="${workflowsLinkPage}">payer</a> on this workspace.`,
currency: 'Currency',
Expand Down Expand Up @@ -2731,6 +2732,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',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ const translations: TranslationDeepObject<typeof en> = {
expensifyLogo: 'Logo de Expensify',
approver: 'Aprobador',
enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `introducir dígito ${digitIndex} de ${totalDigits}`,
agent: 'Agente',
restrictions: 'Restricciones',
},
socials: {
Expand Down Expand Up @@ -2546,6 +2547,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 rapidez',
},
workflowsApprovalLimitPage: {
title: 'Aprobador',
Expand Down
5 changes: 4 additions & 1 deletion src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: 'Année précédente',
nextYear: 'L’an prochain',
avatar: 'Avatar',
agent: 'Agent',
restrictions: 'Restrictions',
},
socials: {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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.',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: 'Anno precedente',
nextYear: "L'anno prossimo",
avatar: 'Avatar',
agent: 'Agente',
restrictions: 'Restrizioni',
},
socials: {
Expand Down Expand Up @@ -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',
Expand Down
5 changes: 4 additions & 1 deletion src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: '前年',
nextYear: '来年',
avatar: 'アバター',
agent: 'エージェント',
restrictions: '制限',
},
socials: {
Expand Down Expand Up @@ -2557,7 +2558,7 @@ ${date} の ${merchant} への ${amount}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `${members} の経費で、承認者は ${approvers} です`,
addApprovalButton: '承認ワークフローを追加',
editWorkflowAction: '編集',
addAgentAction: 'エージェントを追加',
addAgentAction: '担当者を追加',
findWorkflow: 'ワークフローを検索',
addApprovalTip: 'より詳細なワークフローが存在する場合を除き、このデフォルトのワークフローがすべてのメンバーに適用されます。',
approver: '承認者',
Expand Down Expand Up @@ -2644,6 +2645,8 @@ ${date} の ${merchant} への ${amount}`,
genericErrorMessage: '承認者を変更できませんでした。もう一度お試しいただくか、サポートにお問い合わせください。',
title: '承認者を設定',
description: 'この人が経費を承認します。',
createNewAgent: '新しいエージェントを作成',
createNewAgentDescription: '迅速な承認でワークフローを自動化しましょう',
},
workflowsApprovalLimitPage: {
title: '承認者',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: 'Vorig jaar',
nextYear: 'Volgend jaar',
avatar: 'Avatar',
agent: 'Agent',
restrictions: 'Beperkingen',
},
socials: {
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: 'Poprzedni rok',
nextYear: 'W przyszłym roku',
avatar: 'Avatar',
agent: 'Agent',
restrictions: 'Ograniczenia',
},
socials: {
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: 'Ano anterior',
nextYear: 'Ano que vem',
avatar: 'Avatar',
agent: 'Agente',
restrictions: 'Restrições',
},
socials: {
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ const translations: TranslationDeepObject<typeof en> = {
previousYear: '上一年',
nextYear: '明年',
avatar: '头像',
agent: '代理人',
restrictions: '限制',
},
socials: {
Expand Down Expand Up @@ -2591,6 +2592,8 @@ ${amount},商户:${merchant} - 日期:${date}`,
genericErrorMessage: '无法更改审批人。请重试或联系支持。',
title: '设置审批人',
description: '此人将审核并批准这些报销。',
createNewAgent: '创建新代理',
createNewAgentDescription: '使用 Prompt 自动化您的审批',
},
workflowsApprovalLimitPage: {
title: '审批人',
Expand Down
9 changes: 9 additions & 0 deletions src/libs/WorkflowUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions src/libs/actions/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import type {AvatarSource} from '@libs/UserAvatarUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {ApprovalWorkflowOnyx} from '@src/types/onyx';
import type {AnyOnyxUpdate} from '@src/types/onyx/Request';
import {clearApprovalWorkflowApprover} from './Workflow';

function openAgentsPage() {
read(READ_COMMANDS.OPEN_AGENTS_PAGE, null);
Expand Down Expand Up @@ -158,6 +160,20 @@ function clearPendingAgentFromApprovalWorkflow(policyID: string, firstApproverEm
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {[CONST.POLICY.COLLECTION_KEYS.ADD_AGENT]: null}});
}

/**
* Discard a failed optimistic agent that was seeded into the in-progress APPROVAL_WORKFLOW
* (Set Approver flow). Removes the pending approver at `approverIndex`, the optimistic
* personal detail + prompt entry, the optimistic->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});
}
Expand Down Expand Up @@ -358,6 +374,7 @@ export {
createAgent,
clearAgentError,
clearPendingAgentFromApprovalWorkflow,
clearOptimisticAgentFromApprovalWorkflow,
clearAgentUpdateError,
clearAgentNameUpdateError,
clearAgentPromptUpdateError,
Expand Down
59 changes: 55 additions & 4 deletions src/pages/settings/Agents/AddAgentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Comment thread
NicolasBonet marked this conversation as resolved.
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();
Expand Down Expand Up @@ -76,7 +83,15 @@ 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());
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);
};

Expand All @@ -96,7 +111,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);

Expand All @@ -110,6 +125,42 @@ 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,
});
// 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).
// 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));
}
return;
}

Navigation.goBack();
};

Expand Down
Loading
Loading