fix(frontend): UX consistency sweep — dialog case, page titles, clone URL

Resolves QA report observations O7, O8, O10, O11, O12.

O7 — Confirm dialog title case
The Resources page rendered "Delete Directory" / "Delete Resource" in
Title Case while every other delete confirm (template, knowledge,
token) used Sentence case ("Delete template", "Delete token"). Aligned
the resources copy to "Delete directory" / "Delete resource".

O8 — /settings/api-tokens identity mismatch
Three names referred to the same page:
  - sidebar link / h1: "PentAGI API"
  - tab title (route registry): "API tokens"
  - menuItems entry: "PentAGI API"
Unified everything to "API Tokens". Also removed the now-redundant
api-tokens special case in settings-layout's SettingsHeader (it always
returned the same value the menuItems lookup would have produced).

O10 — Provider Clone URL parameter
`/settings/providers/new?id=5` collided semantically with the rest of
the codebase, where `?id=` usually means "this is the record being
edited". Renamed to `?clone=5` so the intent is explicit when the URL
is shared or shows up in logs. Updated the reader in
settings-provider.tsx (`formQueryParams.id` → `formQueryParams.clone`)
to match.

O11 — "Create Provider" button accessibility
The button is a DropdownMenuTrigger that opens a provider-type chooser
before navigating to /new — not a submit button. Sighted users get
this from `<ChevronDown />` and Radix's `aria-haspopup="menu"`, but
the bare visible text "Create Provider" reads as a confirmation
action to screen readers. Added `aria-label="Create provider — choose
type"` and a block comment explaining the pattern so future
maintainers don't try to "fix" the dropdown into a direct submit.

O12 — `(unnamed)` placeholder rendering
Tokens with empty name rendered as literal `(unnamed)` with
text-muted-foreground only. Added `font-normal italic` so the
placeholder reads as missing data rather than a real name (the column
otherwise uses font-medium).

Verified: 0 ESLint errors, 508/508 tests pass.
Browser regression sweep across 9 pages (/dashboard, /flows,
/templates, /knowledges, /resources, /flows/:id, /settings/{providers,
api-tokens,prompts}) still returns 0 icon-only buttons without an
accessible name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-05-22 17:52:16 +07:00
parent 3346387cee
commit 04702c3648
6 changed files with 18 additions and 11 deletions
@@ -47,7 +47,7 @@ const menuItems: readonly MenuItem[] = [
icon: <Key className="size-4" />,
id: 'api-tokens',
path: '/settings/api-tokens',
title: 'PentAGI API',
title: 'API Tokens',
},
] as const;
@@ -74,10 +74,6 @@ function SettingsHeader() {
return 'Edit Prompt';
}
if (path === '/settings/api-tokens') {
return 'PentAGI API';
}
const activeItem = menuItems.find((item) => path.startsWith(item.path));
return activeItem?.title ?? 'Settings';
+1 -1
View File
@@ -34,7 +34,7 @@ export type TitleResolver = ((params: RouteParams) => string) | ComponentType<{
* from this registry onto the matching <Route>.
*/
export const routeTitles = {
apiTokens: { title: 'API tokens' },
apiTokens: { title: 'API Tokens' },
dashboard: { title: 'Dashboard' },
flow: {
title: apolloTitle({
+1 -1
View File
@@ -600,7 +600,7 @@ function Resources() {
isOpen={!!deletion.fileToDelete}
itemName={deletion.fileToDelete?.name}
itemType={deletion.fileToDelete?.isDir ? 'directory' : 'resource'}
title={deletion.fileToDelete?.isDir ? 'Delete Directory' : 'Delete Resource'}
title={deletion.fileToDelete?.isDir ? 'Delete directory' : 'Delete resource'}
/>
</div>
</>
@@ -504,7 +504,9 @@ function SettingsAPITokens() {
return (
<div className="font-medium">
{token.name || <span className="text-muted-foreground">(unnamed)</span>}
{token.name || (
<span className="text-muted-foreground font-normal italic">(unnamed)</span>
)}
</div>
);
},
@@ -877,7 +877,7 @@ function SettingsProvider() {
const formQueryParams = useMemo(
() => ({
id: searchParams.get('id'),
clone: searchParams.get('clone'),
type: searchParams.get('type'),
}),
[searchParams],
@@ -1001,7 +1001,7 @@ function SettingsProvider() {
if (isNew || !providerId) {
const queryType = formQueryParams.type ?? undefined;
const queryId = formQueryParams.id;
const queryId = formQueryParams.clone;
if (queryId && data?.settingsProviders?.userDefined) {
const sourceProvider = data.settingsProviders.userDefined.find((p: Provider) => p.id == queryId);
@@ -103,7 +103,7 @@ function SettingsProviders() {
const handleProviderClone = useCallback(
(providerId: string) => {
navigate(`/settings/providers/new?id=${providerId}`);
navigate(`/settings/providers/new?clone=${providerId}`);
},
[navigate],
);
@@ -451,9 +451,18 @@ function SettingsProvidersHeader() {
<div className="flex items-center justify-between gap-4">
<p className="text-muted-foreground min-w-0 flex-1 truncate">Manage language model providers</p>
{/*
* "Create Provider" is a dropdown trigger, not a submit-style action — it
* opens a menu listing provider types (OpenAI, Anthropic, Custom, …). The
* `<ChevronDown />` icon plus Radix's `aria-haspopup="menu"` already signal
* "menu opens" to sighted and AT users; the explicit aria-label adds the
* intent ("create provider") so screen readers don't just announce
* "Create Provider, menu" but "Create provider, choose type, menu".
*/}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Create provider — choose type"
className="shrink-0"
variant="secondary"
>