Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/admin-inline-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function createThrowawayCustomer(page: Page): Promise<string> {
const form = page.locator('.modal form.stack-form');
await expect(form).toBeVisible();
await form.locator('.field input[type="text"]').first().fill(name);
await form.locator('.field-check').filter({ hasText: /^Global$/ }).locator('input[type="checkbox"]').check();
await form.getByRole('checkbox', { name: 'Global', exact: true }).check();
await form.locator('button[type="submit"]').filter({ hasText: SAVE }).click();
await expect(page.locator('.modal')).toHaveCount(0);
await expect(adminRow(page, name)).toHaveCount(1);
Expand Down
2 changes: 1 addition & 1 deletion e2e/admin/admin-ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ test.describe('Administration UI', () => {
const form = page.locator('.modal form.stack-form');
await expect(form).toBeVisible();
await form.locator('.field input[type="text"]').first().fill(name);
await form.locator('.field-check').filter({ hasText: /^Global$/ }).locator('input[type="checkbox"]').check();
await form.getByRole('checkbox', { name: 'Global', exact: true }).check();
await form.locator('button[type="submit"]').filter({ hasText: SAVE }).click();
await expect(page.locator('.modal')).toHaveCount(0);
await expect(row(page, name)).toHaveCount(1);
Expand Down
6 changes: 6 additions & 0 deletions frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@
"settings_apitoken_scope_resource": "Ressource",
"settings_apitoken_scope_all": "Vollzugriff",
"settings_apitoken_scope_all_hint": "Gewährt alle Berechtigungen Ihres Kontos.",
"settings_apitoken_create_heading": "Neues Token erstellen",
"settings_apitoken_existing_heading": "Ihre Token",
"settings_apitoken_mode_label": "Zugriff",
"settings_apitoken_mode_scoped": "Nach Ressource",
"settings_apitoken_scope_all_warning": "gewährt den Wildcard-Scope (*): Das Token kann alles, was Ihr Konto kann — Lesen und Schreiben auf jeder Ressource, jetzt und auf jeder später hinzugefügten Ressource. Es ist keine Momentaufnahme der heutigen Berechtigungen; neue Rechte werden automatisch abgedeckt.",
"settings_apitoken_select_all": "Alle {action}",
"settings_apitoken_expiry_label": "Läuft ab (optional)",
"settings_apitoken_create": "Token erstellen",
"settings_apitoken_created_title": "Token erstellt",
Expand Down
6 changes: 6 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@
"settings_apitoken_scope_resource": "Resource",
"settings_apitoken_scope_all": "Full access",
"settings_apitoken_scope_all_hint": "Grants every permission your account has.",
"settings_apitoken_create_heading": "Create a new token",
"settings_apitoken_existing_heading": "Your tokens",
"settings_apitoken_mode_label": "Access",
"settings_apitoken_mode_scoped": "By resource",
"settings_apitoken_scope_all_warning": "grants the wildcard scope (*): the token can do everything your account can — read and write on every resource, now and on any resource added later. It is not a snapshot of today's permissions; new capabilities are covered automatically.",
"settings_apitoken_select_all": "Select all {action}",
"settings_apitoken_expiry_label": "Expires (optional)",
"settings_apitoken_create": "Create token",
"settings_apitoken_created_title": "Token created",
Expand Down
6 changes: 6 additions & 0 deletions frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@
"settings_apitoken_scope_resource": "Recurso",
"settings_apitoken_scope_all": "Acceso completo",
"settings_apitoken_scope_all_hint": "Concede todos los permisos de tu cuenta.",
"settings_apitoken_create_heading": "Crear un nuevo token",
"settings_apitoken_existing_heading": "Tus tokens",
"settings_apitoken_mode_label": "Acceso",
"settings_apitoken_mode_scoped": "Por recurso",
"settings_apitoken_scope_all_warning": "concede el ámbito comodín (*): el token puede hacer todo lo que tu cuenta puede — leer y escribir en todos los recursos, ahora y en cualquier recurso añadido más adelante. No es una instantánea de los permisos actuales; las nuevas capacidades se cubren automáticamente.",
"settings_apitoken_select_all": "Seleccionar todo {action}",
"settings_apitoken_expiry_label": "Caduca (opcional)",
"settings_apitoken_create": "Crear token",
"settings_apitoken_created_title": "Token creado",
Expand Down
6 changes: 6 additions & 0 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@
"settings_apitoken_scope_resource": "Ressource",
"settings_apitoken_scope_all": "Accès complet",
"settings_apitoken_scope_all_hint": "Accorde toutes les permissions de votre compte.",
"settings_apitoken_create_heading": "Créer un nouveau jeton",
"settings_apitoken_existing_heading": "Vos jetons",
"settings_apitoken_mode_label": "Accès",
"settings_apitoken_mode_scoped": "Par ressource",
"settings_apitoken_scope_all_warning": "accorde la portée générique (*) : le jeton peut tout faire ce que votre compte peut faire — lire et écrire sur chaque ressource, maintenant et sur toute ressource ajoutée ultérieurement. Ce n'est pas un instantané des permissions actuelles ; les nouvelles capacités sont couvertes automatiquement.",
"settings_apitoken_select_all": "Tout sélectionner {action}",
"settings_apitoken_expiry_label": "Expire (facultatif)",
"settings_apitoken_create": "Créer le jeton",
"settings_apitoken_created_title": "Jeton créé",
Expand Down
6 changes: 6 additions & 0 deletions frontend/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@
"settings_apitoken_scope_resource": "Ресурс",
"settings_apitoken_scope_all": "Полный доступ",
"settings_apitoken_scope_all_hint": "Предоставляет все разрешения вашей учётной записи.",
"settings_apitoken_create_heading": "Создать новый токен",
"settings_apitoken_existing_heading": "Ваши токены",
"settings_apitoken_mode_label": "Доступ",
"settings_apitoken_mode_scoped": "По ресурсу",
"settings_apitoken_scope_all_warning": "предоставляет подстановочную область (*): токен может делать всё, что доступно вашей учётной записи — чтение и запись для любого ресурса, сейчас и для любого ресурса, добавленного позже. Это не снимок текущих разрешений; новые возможности покрываются автоматически.",
"settings_apitoken_select_all": "Выбрать все {action}",
"settings_apitoken_expiry_label": "Истекает (необязательно)",
"settings_apitoken_create": "Создать токен",
"settings_apitoken_created_title": "Токен создан",
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/AdminCrudShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -940,11 +940,13 @@ function FieldControl(props: {
<Switch>
<Match when={props.field.type === 'checkbox'}>
<div class="field-check-row">
<label class="field-check">
<input type="checkbox" checked={Boolean(value())} onInput={(e) => props.setField(props.field.name, e.currentTarget.checked)} />
<span>{props.field.label()}</span>
<label class="field-check-label" for={`field-check-${props.field.name}`}>
{props.field.label()}
</label>
<FieldHelp field={props.field} />
<div class="field-check">
<input id={`field-check-${props.field.name}`} type="checkbox" checked={Boolean(value())} onInput={(e) => props.setField(props.field.name, e.currentTarget.checked)} />
<FieldHelp field={props.field} />
</div>
</div>
</Match>
<Match when={props.field.type === 'multiselect'}>
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/ApiTokenControls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,42 @@ describe('ApiTokenControls', () => {
)
})

it('column select-all fills its action column and shows a three-state header', async () => {
render(() => <ApiTokenControls />)
await waitFor(() => screen.getByLabelText('entries:read'))

const selectAllRead = screen.getByLabelText('Select all read') as HTMLInputElement

// One of two read cells ticked → the header is partial (indeterminate).
fireEvent.change(screen.getByLabelText('entries:read'), { target: { checked: true } })
await waitFor(() => expect(selectAllRead.indeterminate).toBe(true))
expect(selectAllRead.checked).toBe(false)

// The header select-all fills the whole read column, no longer partial.
fireEvent.change(selectAllRead, { target: { checked: true } })
expect(screen.getByLabelText('entries:read')).toBeChecked()
expect(screen.getByLabelText('projects:read')).toBeChecked()
expect(screen.getByLabelText('entries:write')).not.toBeChecked()
await waitFor(() => expect(selectAllRead.indeterminate).toBe(false))
expect(selectAllRead.checked).toBe(true)

// Clearing the header empties the column again.
fireEvent.change(selectAllRead, { target: { checked: false } })
expect(screen.getByLabelText('entries:read')).not.toBeChecked()
expect(screen.getByLabelText('projects:read')).not.toBeChecked()
})

it('full-access mode hides the scope grid and shows the wildcard warning', async () => {
render(() => <ApiTokenControls />)
await waitFor(() => screen.getByLabelText('entries:read'))

fireEvent.change(screen.getByLabelText('Full access', { exact: false }), { target: { checked: true } })

// The grid is gone and the caution note takes its place.
expect(screen.queryByLabelText('entries:read')).not.toBeInTheDocument()
expect(screen.getByRole('note')).toBeInTheDocument()
})

it('applies a preset as exactly its scope set and marks it active', async () => {
listApiTokens.mockResolvedValue({ tokens: [], resources: ['entries', 'sync'], actions: ['read', 'write'], wildcard: '*' })
render(() => <ApiTokenControls />)
Expand Down
Loading
Loading