From bf2b5ad2a09262c7d74da370b7c4b5124c1649df Mon Sep 17 00:00:00 2001 From: denispetre Date: Thu, 23 Jul 2026 11:53:58 +0300 Subject: [PATCH 1/6] fix(apollo-react): read Discovery's real CostDetails shape for cost badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway nests token costs as costDetails.flatCosts.inputTokenCost (cents per million tokens; tieredCosts for tiered pricing) — the picker read a flat inputTokenCost that never exists, so default cost badges never rendered. Types now match the DTO and defaultCostTier bins the nested value at 100/500 cents per million. Co-Authored-By: Claude Fable 5 --- .../ap-model-picker/ModelPicker.stories.tsx | 28 +++++------------ .../components/ap-model-picker/types.ts | 18 +++++++++-- .../components/ap-model-picker/utils.test.ts | 30 +++++++++++-------- .../components/ap-model-picker/utils.ts | 11 ++++--- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx index 876843dd1..1366d7de0 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx @@ -25,9 +25,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ // would be wrong; we bump it via the test rate to demonstrate the // boundary). Real gateway rates here. costDetails: { - inputTokenCost: 3.0, - outputTokenCost: 15.0, - currency: 'USD', + flatCosts: { inputTokenCost: 300, outputTokenCost: 1500 }, }, }, }, @@ -40,9 +38,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ modelDetails: { contextWindowTokens: 200000, costDetails: { - inputTokenCost: 3.0, - outputTokenCost: 15.0, - currency: 'USD', + flatCosts: { inputTokenCost: 300, outputTokenCost: 1500 }, }, }, }, @@ -56,9 +52,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ modelDetails: { contextWindowTokens: 1000000, costDetails: { - inputTokenCost: 0.35, - outputTokenCost: 1.5, - currency: 'USD', + flatCosts: { inputTokenCost: 35, outputTokenCost: 150 }, }, }, }, @@ -74,9 +68,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ maxOutputTokens: 128000, // gpt-5 large: premium costDetails: { - inputTokenCost: 6.0, - outputTokenCost: 18.0, - currency: 'USD', + flatCosts: { inputTokenCost: 600, outputTokenCost: 1800 }, }, }, }, @@ -94,9 +86,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ contextWindowTokens: 128000, maxOutputTokens: 16384, costDetails: { - inputTokenCost: 2.5, - outputTokenCost: 10.0, - currency: 'USD', + flatCosts: { inputTokenCost: 250, outputTokenCost: 1000 }, }, }, }, @@ -189,9 +179,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ maxOutputTokens: 32768, // gpt-4.1-mini: basic costDetails: { - inputTokenCost: 0.4, - outputTokenCost: 1.6, - currency: 'USD', + flatCosts: { inputTokenCost: 40, outputTokenCost: 160 }, }, }, }, @@ -213,9 +201,7 @@ const MOCK_MODELS: DiscoveryModel[] = [ contextWindowTokens: 400000, maxOutputTokens: 128000, costDetails: { - inputTokenCost: 2.5, - outputTokenCost: 10.0, - currency: 'USD', + flatCosts: { inputTokenCost: 250, outputTokenCost: 1000 }, }, }, }, diff --git a/packages/apollo-react/src/material/components/ap-model-picker/types.ts b/packages/apollo-react/src/material/components/ap-model-picker/types.ts index 2a5d6096d..f9aaaa7d9 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/types.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/types.ts @@ -57,10 +57,24 @@ export interface ByomDetails { byoConfigurationId?: string; } -export interface ModelCostDetails { +/** Token costs in cents per million tokens (the gateway's unit). */ +export interface ModelFlatCosts { inputTokenCost?: number; outputTokenCost?: number; - currency?: string; + cacheReadInputTokenCost?: number; + cacheWriteInputTokenCost?: number; +} + +export interface ModelTieredCost extends ModelFlatCosts { + minimumTokens?: number; + maximumTokens?: number; +} + +export interface ModelCostDetails { + /** `Flat` | `Tiered` */ + pricingType?: string; + flatCosts?: ModelFlatCosts; + tieredCosts?: ModelTieredCost[]; } export interface ModelDetails { diff --git a/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts b/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts index e443720e2..271ceb18e 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/utils.test.ts @@ -246,7 +246,7 @@ describe('deriveModelTags', () => { const tags = deriveModelTags( model({ modelId: 'a', - modelDetails: { costDetails: { inputTokenCost: 6 } }, + modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } }, }) ); expect(tags.find((t) => t.kind === 'cost-premium')).toBeTruthy(); @@ -259,7 +259,7 @@ describe('deriveModelTags', () => { const suppressed = deriveModelTags( model({ modelId: 'a', - modelDetails: { costDetails: { inputTokenCost: 6 } }, + modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } }, }), { badgesFor: () => [] } ); @@ -274,7 +274,7 @@ describe('deriveModelTags', () => { const tags = deriveModelTags( model({ modelId: 'a', - modelDetails: { costDetails: { inputTokenCost: 6 } }, + modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } }, }), { customTagsFor: costBadges } ); @@ -305,15 +305,21 @@ describe('defaultCostTier', () => { }); it('bins at < $1 / < $5 / >= $5 thresholds', () => { - expect(defaultCostTier(model({ modelDetails: { costDetails: { inputTokenCost: 0.4 } } }))).toBe( - 'basic' - ); - expect(defaultCostTier(model({ modelDetails: { costDetails: { inputTokenCost: 3 } } }))).toBe( - 'standard' - ); - expect(defaultCostTier(model({ modelDetails: { costDetails: { inputTokenCost: 6 } } }))).toBe( - 'premium' - ); + expect( + defaultCostTier( + model({ modelDetails: { costDetails: { flatCosts: { inputTokenCost: 40 } } } }) + ) + ).toBe('basic'); + expect( + defaultCostTier( + model({ modelDetails: { costDetails: { flatCosts: { inputTokenCost: 300 } } } }) + ) + ).toBe('standard'); + expect( + defaultCostTier( + model({ modelDetails: { costDetails: { flatCosts: { inputTokenCost: 600 } } } }) + ) + ).toBe('premium'); }); }); diff --git a/packages/apollo-react/src/material/components/ap-model-picker/utils.ts b/packages/apollo-react/src/material/components/ap-model-picker/utils.ts index ad4659229..f4ce32000 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/utils.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/utils.ts @@ -250,15 +250,18 @@ export function deriveModelTags( * return tier ? [`cost-${tier}` as const] : []; * }} * - * Bins Discovery's `inputTokenCost` (USD per million input tokens) at + * Bins Discovery's `costDetails.flatCosts.inputTokenCost` (cents per million input tokens) at * $1 / $5. Copy it and change the thresholds, or classify on something * else entirely — the pool badge kinds are the shared contract, the * classifier is yours. */ -const DEFAULT_BASIC_THRESHOLD = 1.0; -const DEFAULT_PREMIUM_THRESHOLD = 5.0; +// Cents per million input tokens: $1/M and $5/M. +const DEFAULT_BASIC_THRESHOLD = 100; +const DEFAULT_PREMIUM_THRESHOLD = 500; export function defaultCostTier(model: DiscoveryModel): CostTier | null { - const inputCost = model.modelDetails?.costDetails?.inputTokenCost; + const costDetails = model.modelDetails?.costDetails; + const inputCost = + costDetails?.flatCosts?.inputTokenCost ?? costDetails?.tieredCosts?.[0]?.inputTokenCost; if (inputCost == null) return null; if (inputCost < DEFAULT_BASIC_THRESHOLD) return 'basic'; if (inputCost < DEFAULT_PREMIUM_THRESHOLD) return 'standard'; From 92a8c49f828289fb0833651aa203c8d301c8008a Mon Sep 17 00:00:00 2001 From: denispetre Date: Thu, 23 Jul 2026 11:54:15 +0300 Subject: [PATCH 2/6] feat(apollo-react): confirm before deleting a BYO configuration Deleting a configuration affects every consumer in the tenant, so the picker now shows a built-in confirmation dialog (naming the configuration) before invoking onDeleteModel. The dialog honors the shadow-DOM container/disablePortal plumbing; strings ship in all picker catalogs. Co-Authored-By: Claude Fable 5 --- .../ap-model-picker/ModelPicker.test.tsx | 13 ++++- .../ap-model-picker/ModelPicker.tsx | 53 ++++++++++++++++--- .../components/ap-model-picker/README.md | 4 +- .../components/ap-model-picker/i18n.ts | 24 +++++++++ .../ap-model-picker/locales/de.json | 6 ++- .../ap-model-picker/locales/en.json | 6 ++- .../ap-model-picker/locales/es-MX.json | 6 ++- .../ap-model-picker/locales/es.json | 6 ++- .../ap-model-picker/locales/fr.json | 6 ++- .../ap-model-picker/locales/ja.json | 6 ++- .../ap-model-picker/locales/ko.json | 6 ++- .../ap-model-picker/locales/pt-BR.json | 6 ++- .../ap-model-picker/locales/pt.json | 6 ++- .../ap-model-picker/locales/ro.json | 7 ++- .../ap-model-picker/locales/ru.json | 6 ++- .../ap-model-picker/locales/tr.json | 6 ++- .../ap-model-picker/locales/zh-CN.json | 6 ++- .../ap-model-picker/locales/zh-TW.json | 6 ++- 18 files changed, 155 insertions(+), 24 deletions(-) diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx index 2c97bba1e..2366e3c98 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.test.tsx @@ -253,7 +253,7 @@ describe('', () => { } }); - it('renders a delete row action on BYO rows when onDeleteModel is provided', async () => { + it('delete row action asks for confirmation before invoking onDeleteModel', async () => { const user = userEvent.setup(); const onDeleteModel = vi.fn(); renderPicker( @@ -267,9 +267,18 @@ describe('', () => { ); await user.click(screen.getByRole('button', { expanded: false })); const deleteButton = await screen.findByRole('button', { name: /delete configuration/i }); + + // Cancel: nothing happens. await user.click(deleteButton); + await user.click(await screen.findByRole('button', { name: /cancel/i })); + expect(onDeleteModel).not.toHaveBeenCalled(); + + // Confirm: the host handler runs with the BYO row. Opening the dialog + // closes the popup, so reopen it once the dialog's aria-hidden lifts. + await user.click(await screen.findByRole('button', { expanded: false })); + await user.click(await screen.findByRole('button', { name: /delete configuration/i })); + await user.click(await screen.findByRole('button', { name: /^delete$/i })); expect(onDeleteModel).toHaveBeenCalledTimes(1); - // Called with the BYO model row. expect(onDeleteModel.mock.calls[0][0]).toMatchObject({ modelSubscriptionType: 'BYOMAdded' }); }); diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx index fc6540554..dfe69cfb9 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx @@ -1,11 +1,18 @@ import AddIcon from '@mui/icons-material/Add'; import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import ButtonBase from '@mui/material/ButtonBase'; import CircularProgress from '@mui/material/CircularProgress'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; import Typography from '@mui/material/Typography'; import { Colors, FontFamily } from '@uipath/apollo-core'; import React from 'react'; import { useSafeLingui } from '../../../i18n'; +import { DELETE_CONFIRM } from './i18n'; import type { ModelBadgeKind } from './badges'; import { FolderSwitcher, type FolderSwitcherFolder } from './primitives/FolderSwitcher'; import { defaultRowActions } from './primitives/ModelOptionRow'; @@ -424,14 +431,20 @@ export const ModelPicker = React.forwardRef // In self-fetch mode a BYO delete refetches the catalog once the // host's handler resolves; with host-owned models the host refreshes. + // Deleting a configuration affects every consumer in the tenant, so the + // picker always confirms before invoking the host's handler. + const [pendingDelete, setPendingDelete] = React.useState(null); const handleDeleteModel = React.useMemo(() => { if (!onDeleteModel) return undefined; - if (!selfFetchCtx) return onDeleteModel; - return async (m: DiscoveryModel) => { - await onDeleteModel(m); - refetchDiscovery(); - }; - }, [onDeleteModel, selfFetchCtx, refetchDiscovery]); + return (m: DiscoveryModel) => setPendingDelete(m); + }, [onDeleteModel]); + const confirmPendingDelete = React.useCallback(async () => { + const model = pendingDelete; + setPendingDelete(null); + if (!model) return; + await onDeleteModel?.(model); + if (selfFetchCtx) refetchDiscovery(); + }, [pendingDelete, onDeleteModel, selfFetchCtx, refetchDiscovery]); // BYO rows without a host-supplied `byoConnectionLabel` get their // Integration Service connection name resolved by the picker itself. @@ -896,6 +909,34 @@ export const ModelPicker = React.forwardRef )} + + setPendingDelete(null)} + container={popupContainer} + disablePortal={disablePortal} + maxWidth="xs" + > + + {_(DELETE_CONFIRM.title)} + + + + {_({ + ...DELETE_CONFIRM.message, + values: { + name: pendingDelete ? (pendingDelete.displayName ?? pendingDelete.modelName) : '', + }, + })} + + + + + + + ); } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/README.md b/packages/apollo-react/src/material/components/ap-model-picker/README.md index 4729288f4..75cc709dc 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/README.md +++ b/packages/apollo-react/src/material/components/ap-model-picker/README.md @@ -154,7 +154,7 @@ Resolution order: `recommendedModelIds` prop (test/storybook override) → DTO ` ### 5. Cost badges -Cost badges are **on by default**: whenever the Discovery DTO carries cost data, each row gets its tier from the Apollo pool (`cost-basic` / `cost-standard` / `cost-premium`) via `defaultCostTier`, which bins `modelDetails.costDetails.inputTokenCost` (USD per million input tokens) at `$1` / `$5`. No wiring needed. +Cost badges are **on by default**: whenever the Discovery DTO carries cost data, each row gets its tier from the Apollo pool (`cost-basic` / `cost-standard` / `cost-premium`) via `defaultCostTier`, which bins `modelDetails.costDetails.flatCosts.inputTokenCost` (cents per million input tokens, the gateway's unit; the first tier is used for tiered pricing) at `$1`/M and `$5`/M. No wiring needed. A host-supplied `badgesFor` takes over entirely — return your own pool kinds to reclassify, or `[]` to suppress badges: @@ -185,7 +185,7 @@ Under the hood: `GET {baseUrl}/portal_/api/organization/UserOrganizationInfo` - The **"Use custom model" footer** opens the *add configuration* form, deep-linked to `/{tenantId}/{folderId}/add` and pre-populated via `?product=&feature=` from `requestingProduct`/`requestingFeature`. The folder id is the switcher's current selection, or — when none is selected ("All folders") — the first available folder, matching the configurations page's own default. Only when no folders exist does it fall back to the configurations list. - The **edit row action** (BYO rows only) opens the configuration's *edit* form when the model carries `byomDetails.byoConfigurationId` (served by Discovery on UiPath/Arima#2659); when absent it lands on the configurations list scoped to the tenant + folder. -- The **delete row action** (BYO rows only) is opt-in: pass `onDeleteModel` and the picker renders a delete icon next to edit, calling your handler (which performs the deletion and refreshes `models`). Omit it to keep removal on the configurations page only. +- The **delete row action** (BYO rows only) is opt-in: pass `onDeleteModel` and the picker renders a delete icon next to edit. The picker always asks for confirmation first (a built-in dialog naming the configuration), then calls your handler; in self-fetch mode it refetches Discovery once your handler resolves. Omit it to keep removal on the configurations page only. These navigations always open the AI Trust Layer pages in a new browser tab - the picker is embedded in a product surface, and navigating it away would unload the user's in-progress work. `onUseCustomModel` overrides the footer's default navigation (e.g. an in-app wizard), and `slots.optionActions` overrides the row actions. Products with their own authorization model can pass `canManageByo` (`true`/`false`); when set, no admin check request is made. diff --git a/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts b/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts index 56a52e9a5..4ffe4fb31 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts +++ b/packages/apollo-react/src/material/components/ap-model-picker/i18n.ts @@ -160,6 +160,30 @@ export const ROW_ACTIONS = { }), } as const; +/* ────────────────────────────────────────────────────────────────────── + * Delete confirmation + * ─────────────────────────────────────────────────────────────────── */ + +export const DELETE_CONFIRM = { + title: msg({ + id: 'modelPicker.deleteConfirm.title', + message: 'Delete custom model', + }), + message: msg({ + id: 'modelPicker.deleteConfirm.message', + message: + 'This permanently deletes the "{name}" configuration for everyone in this tenant. This action cannot be undone.', + }), + cancel: msg({ + id: 'modelPicker.deleteConfirm.cancel', + message: 'Cancel', + }), + confirm: msg({ + id: 'modelPicker.deleteConfirm.confirm', + message: 'Delete', + }), +} as const; + /* ────────────────────────────────────────────────────────────────────── * Footer CTA * ─────────────────────────────────────────────────────────────────── */ diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/de.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json index 3a951a395..bfcffc4ba 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/en.json @@ -42,5 +42,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/es-MX.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/es.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/fr.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ja.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ko.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt-BR.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/pt.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json index 0967ef424..1d74dc163 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ro.json @@ -1 +1,6 @@ -{} +{ + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" +} diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/ru.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/tr.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-CN.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } diff --git a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json index 544693085..3f709ab0c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json +++ b/packages/apollo-react/src/material/components/ap-model-picker/locales/zh-TW.json @@ -41,5 +41,9 @@ "modelPicker.useCustomModel.title": "Use custom model", "modelPicker.tag.deprecating.tooltip": "Will be replaced by {replacement}", "modelPicker.empty.noMatch": "No models match \"{query}\".", - "modelPicker.empty.noModels": "No models available." + "modelPicker.empty.noModels": "No models available.", + "modelPicker.deleteConfirm.title": "Delete custom model", + "modelPicker.deleteConfirm.message": "This permanently deletes the \"{name}\" configuration for everyone in this tenant. This action cannot be undone.", + "modelPicker.deleteConfirm.cancel": "Cancel", + "modelPicker.deleteConfirm.confirm": "Delete" } From 52ec781dffd21786147d3a0aa4858144d7a221e2 Mon Sep 17 00:00:00 2001 From: denispetre Date: Thu, 23 Jul 2026 17:44:05 +0300 Subject: [PATCH 3/6] docs(apollo-react): story for the delete confirmation flow Co-Authored-By: Claude Fable 5 --- .../ap-model-picker/ModelPicker.stories.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx index 1366d7de0..b0421ccc1 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.stories.tsx @@ -363,6 +363,30 @@ export const AdminCanManageByo: Story = { }, }; +export const DeleteWithConfirmation: Story = { + name: 'Admin: delete asks for confirmation', + render: Controlled, + args: { + variant: 'searchable', + models: MOCK_MODELS, + canManageByo: true, + onDeleteModel: (model: DiscoveryModel) => { + // eslint-disable-next-line no-console + console.log('[story] confirmed delete of', model.modelId); + }, + }, + parameters: { + docs: { + description: { + story: + 'Pass `onDeleteModel` and BYO rows gain a delete action. The picker ' + + 'always shows a confirmation dialog naming the configuration before ' + + 'invoking the handler — deletion affects every consumer in the tenant.', + }, + }, + }, +}; + export const ViewerCannotManageByo: Story = { name: 'Viewer: read-only BYO', render: Controlled, From e6a2822757cf3ad1839f19bf147590e91468ca4a Mon Sep 17 00:00:00 2001 From: denispetre Date: Thu, 23 Jul 2026 17:51:08 +0300 Subject: [PATCH 4/6] fix(apollo-react): delete dialog stacks above the picker popup Co-Authored-By: Claude Fable 5 --- .../src/material/components/ap-model-picker/ModelPicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx index dfe69cfb9..e192e3914 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx @@ -916,6 +916,7 @@ export const ModelPicker = React.forwardRef container={popupContainer} disablePortal={disablePortal} maxWidth="xs" + sx={{ zIndex: 1500 }} > {_(DELETE_CONFIRM.title)} From ca20b5f383c83dde6b080406596d700425f93ed0 Mon Sep 17 00:00:00 2001 From: denispetre Date: Fri, 24 Jul 2026 09:46:10 +0300 Subject: [PATCH 5/6] =?UTF-8?q?fix(apollo-react):=20delete=20dialog=20?= =?UTF-8?q?=E2=80=94=20alertdialog=20semantics=20+=20Apollo=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit role=alertdialog wired to its title/description ids, focus starts on the safe action, and the paper uses the picker's design tokens (background-raised/foreground vars, 10px radius, Apollo font) so it matches the dropdown panel and follows the host theme. Axe-clean on WCAG 2.1 A/AA. Co-Authored-By: Claude Fable 5 --- .../ap-model-picker/ModelPicker.tsx | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx index e192e3914..194974c4c 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx @@ -916,13 +916,44 @@ export const ModelPicker = React.forwardRef container={popupContainer} disablePortal={disablePortal} maxWidth="xs" + // Destructive interrupt: announce as an alert dialog wired to its + // own title and description. + role="alertdialog" + aria-labelledby={`${id}-delete-title`} + aria-describedby={`${id}-delete-description`} sx={{ zIndex: 1500 }} + PaperProps={{ + sx: { + borderRadius: '10px', + border: '1px solid', + borderColor: `var(--color-border-de-emp, ${Colors.ColorGray300})`, + backgroundColor: `var(--color-background-raised, ${Colors.ColorWhite})`, + backgroundImage: 'none', + boxShadow: '0 12px 30px rgba(16, 24, 40, 0.10), 0 2px 6px rgba(16, 24, 40, 0.04)', + fontFamily: FontFamily.FontNormal, + }, + }} > - + {_(DELETE_CONFIRM.title)} - + {_({ ...DELETE_CONFIRM.message, values: { @@ -931,8 +962,11 @@ export const ModelPicker = React.forwardRef })} - - + + {/* Focus lands on the safe action for a destructive confirm. */} + From 2ea3b0b0ee955c2cf1d4ca52d7f57f0a9082a7da Mon Sep 17 00:00:00 2001 From: denispetre Date: Fri, 24 Jul 2026 16:57:43 +0300 Subject: [PATCH 6/6] style(apollo-react): delete dialog visuals defer to the host MUI theme Plain Dialog structure (secondary Cancel + contained destructive action, disableElevation) matching how products' own confirmation dialogs render under the Apollo MUI theme; alertdialog semantics and safe-action focus kept. Co-Authored-By: Claude Fable 5 --- .../ap-model-picker/ModelPicker.tsx | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx index 194974c4c..af23b2142 100644 --- a/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx +++ b/packages/apollo-react/src/material/components/ap-model-picker/ModelPicker.tsx @@ -917,43 +917,20 @@ export const ModelPicker = React.forwardRef disablePortal={disablePortal} maxWidth="xs" // Destructive interrupt: announce as an alert dialog wired to its - // own title and description. + // own title and description. Visuals are deliberately left to the + // host's MUI theme so the dialog matches the product's other + // confirmation dialogs. role="alertdialog" aria-labelledby={`${id}-delete-title`} aria-describedby={`${id}-delete-description`} sx={{ zIndex: 1500 }} - PaperProps={{ - sx: { - borderRadius: '10px', - border: '1px solid', - borderColor: `var(--color-border-de-emp, ${Colors.ColorGray300})`, - backgroundColor: `var(--color-background-raised, ${Colors.ColorWhite})`, - backgroundImage: 'none', - boxShadow: '0 12px 30px rgba(16, 24, 40, 0.10), 0 2px 6px rgba(16, 24, 40, 0.04)', - fontFamily: FontFamily.FontNormal, - }, - }} + PaperProps={{ sx: { fontFamily: FontFamily.FontNormal } }} > - + {_(DELETE_CONFIRM.title)} - + {_({ ...DELETE_CONFIRM.message, values: { @@ -962,12 +939,22 @@ export const ModelPicker = React.forwardRef })} - + {/* Focus lands on the safe action for a destructive confirm. */} - -