mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-12 18:15:33 +00:00
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:
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/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;
|
||||
Reference in new issue
Block a user