From b1ff8f516e60fc787014feb6c49cf6ee27410865 Mon Sep 17 00:00:00 2001 From: arc53-machine <232052973+arc53-machine@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:32:46 +0100 Subject: [PATCH] fix(frontend): access tokens dates and names render unescaped, align the scopes block i18next HTML-escaped interpolated values, so expiry dates showed as 26/09/2026 and token names containing & or quotes were mangled in the created and revoke dialogs; React already escapes on render, so those strings opt out like utils/streamingStatusUtils does. The scopes fieldset drops the browser's default padding so it lines up with the other fields, and the native checkboxes follow the dark colour scheme. --- frontend/src/modals/AccessTokenCreatedModal.tsx | 6 +++++- frontend/src/modals/CreateAccessTokenModal.tsx | 6 ++++-- frontend/src/settings/PersonalAccessTokens.tsx | 11 ++++++++--- frontend/src/settings/accessTokenUtils.test.ts | 7 +++++++ frontend/src/settings/accessTokenUtils.ts | 8 ++++++++ 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/frontend/src/modals/AccessTokenCreatedModal.tsx b/frontend/src/modals/AccessTokenCreatedModal.tsx index 66026edc..2b627f93 100644 --- a/frontend/src/modals/AccessTokenCreatedModal.tsx +++ b/frontend/src/modals/AccessTokenCreatedModal.tsx @@ -5,6 +5,7 @@ import { baseURL } from '../api/client'; import CopyButton from '../components/CopyButton'; import { Button } from '../components/ui/button'; import { Modal } from '../components/ui/modal'; +import { NO_ESCAPE } from '../settings/accessTokenUtils'; interface AccessTokenCreatedModalProps { /** Plaintext secret; `null` keeps the modal closed. Held only in the parent's component state. */ @@ -49,7 +50,10 @@ export default function AccessTokenCreatedModal({ {t('settings.accessTokens.created.title')}

- {t('settings.accessTokens.created.subtitle', { name })} + {t('settings.accessTokens.created.subtitle', { + name, + ...NO_ESCAPE, + })}

diff --git a/frontend/src/modals/CreateAccessTokenModal.tsx b/frontend/src/modals/CreateAccessTokenModal.tsx index 438403a8..87e39a2d 100644 --- a/frontend/src/modals/CreateAccessTokenModal.tsx +++ b/frontend/src/modals/CreateAccessTokenModal.tsx @@ -30,6 +30,7 @@ import { expiryOptions, groupScopesByFamily, isScopeImplied, + NO_ESCAPE, NO_EXPIRY, PICKER_FAMILIES, ResourceOption, @@ -306,12 +307,13 @@ export default function CreateAccessTokenModal({ date: formatDateOnly( new Date(Date.now() + expiry * DAY_MS).toISOString(), ), + ...NO_ESCAPE, })}

-
+
{t('settings.accessTokens.create.scopes')} * @@ -355,7 +357,7 @@ export default function CreateAccessTokenModal({ checked={checked} disabled={implied} onChange={() => toggleScope(scope.name)} - className="accent-primary mt-0.5 size-4 shrink-0 rounded-sm border-gray-300 bg-transparent" + className="accent-primary mt-0.5 size-4 shrink-0 rounded-sm border-gray-300 bg-transparent dark:[color-scheme:dark]" /> diff --git a/frontend/src/settings/PersonalAccessTokens.tsx b/frontend/src/settings/PersonalAccessTokens.tsx index d81416b0..7439e567 100644 --- a/frontend/src/settings/PersonalAccessTokens.tsx +++ b/frontend/src/settings/PersonalAccessTokens.tsx @@ -33,6 +33,7 @@ import { formatDateOnly, formatDateTime } from '../utils/dateTimeUtils'; import { countLiveTokens, expiryStatus, + NO_ESCAPE, relativeTime, restrictionCounts, } from './accessTokenUtils'; @@ -197,8 +198,8 @@ export default function PersonalAccessTokens() { > ); }; @@ -210,7 +211,10 @@ export default function PersonalAccessTokens() { size="sm" className="text-destructive hover:text-destructive border-destructive/40 hover:bg-destructive/10 rounded-full px-4" onClick={() => requestRevoke(item)} - aria-label={t('settings.accessTokens.revokeAria', { name: item.name })} + aria-label={t('settings.accessTokens.revokeAria', { + name: item.name, + ...NO_ESCAPE, + })} > {t('settings.accessTokens.revoke')} @@ -431,6 +435,7 @@ export default function PersonalAccessTokens() { { expect(toResourceOptions('agents', { success: false })).toEqual([]); }); }); + +describe('NO_ESCAPE', () => { + it('turns off i18next HTML escaping so dates and names render as typed', () => { + expect(NO_ESCAPE).toEqual({ interpolation: { escapeValue: false } }); + }); +}); diff --git a/frontend/src/settings/accessTokenUtils.ts b/frontend/src/settings/accessTokenUtils.ts index 134d347d..9e7e56c3 100644 --- a/frontend/src/settings/accessTokenUtils.ts +++ b/frontend/src/settings/accessTokenUtils.ts @@ -222,3 +222,11 @@ export function toResourceOptions( })) .sort((a, b) => a.label.localeCompare(b.label)); } + +/** + * i18next HTML-escapes interpolated values by default, which turns a date like + * 26/09/2026 into `26/09/2026` and mangles token names containing + * `&` or `'`. React already escapes on render, so opt out for those values + * (same approach as utils/streamingStatusUtils.ts). + */ +export const NO_ESCAPE = { interpolation: { escapeValue: false } } as const;