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.
This commit is contained in:
arc53-machine committed 2026-09-20 13:32:46 +01:00
1 parent 7ddb5f6400
commit b1ff8f516e
5 files changed
+32 -6

No files matched your search

@@ -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')}
</h2>
<p className="text-muted-foreground mt-2 text-sm break-words">
{t('settings.accessTokens.created.subtitle', { name })}
{t('settings.accessTokens.created.subtitle', {
name,
...NO_ESCAPE,
})}
</p>
</div>
@@ -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,
})}
</p>
</div>
</div>
<fieldset className="flex flex-col gap-3">
<fieldset className="m-0 flex min-w-0 flex-col gap-3 border-0 p-0">
<legend className="text-foreground dark:text-foreground text-sm font-semibold">
{t('settings.accessTokens.create.scopes')}
<span className="text-red-500">*</span>
@@ -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]"
/>
<span className="flex min-w-0 flex-col gap-0.5">
<code className="text-foreground dark:text-foreground font-mono text-xs font-medium">
@@ -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() {
>
<TriangleAlert className="size-3 shrink-0" aria-hidden="true" />
{expired
? t('settings.accessTokens.expired', { date })
: t('settings.accessTokens.expiresSoon', { date })}
? t('settings.accessTokens.expired', { date, ...NO_ESCAPE })
: t('settings.accessTokens.expiresSoon', { date, ...NO_ESCAPE })}
</span>
);
};
@@ -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')}
</Button>
@@ -431,6 +435,7 @@ export default function PersonalAccessTokens() {
<ConfirmationModal
message={t('settings.accessTokens.revokeWarning', {
name: tokenToRevoke?.name ?? '',
...NO_ESCAPE,
})}
modalState={revokeState}
setModalState={setRevokeState}
@@ -10,6 +10,7 @@ import {
groupScopesByFamily,
isScopeImplied,
isUuid,
NO_ESCAPE,
NO_EXPIRY,
relativeTime,
restrictionCounts,
@@ -287,3 +288,9 @@ describe('toResourceOptions', () => {
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 } });
});
});
@@ -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&#x2F;09&#x2F;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;