diff --git a/frontend/src/lib/api/endpoints/grants.ts b/frontend/src/lib/api/endpoints/grants.ts index 6c880e8a..cc96c0e1 100644 --- a/frontend/src/lib/api/endpoints/grants.ts +++ b/frontend/src/lib/api/endpoints/grants.ts @@ -7,7 +7,9 @@ import type { ResourceBody, ResourcePage } from './resources'; const JSON_HEADERS = { 'Content-Type': 'application/json' }; export type SubjectType = 'user' | 'group' | 'email' | 'token'; -export type ShareRole = 'viewer' | 'editor' | 'admin'; +/** Roles the share UI exposes. The backend role enum also has `commenter` and + * `contributor`, which {@link displayRole} collapses to the nearest of these. */ +export type ShareRole = 'viewer' | 'editor' | 'owner'; export interface GrantSubject { type: SubjectType; @@ -25,13 +27,17 @@ export type GrantSubjectInput = | { type: 'token'; id: string } | { type: 'email'; email: string }; -/** One grant carries a single permission; a subject's role is derived from all of theirs. */ +/** + * One role grant for a (subject, resource). Role-keyed since the role-grants + * migration: each row carries an explicit `role` (the backend enum, which may + * be `owner`/`editor`/`viewer`/`commenter`/`contributor`). + */ export interface Grant { id: string; granted_at?: string; granted_by?: string; subject: GrantSubject; - permission: string; + role: string; resource: { type: ItemType; id: string }; expires_at?: string | null; } @@ -56,10 +62,16 @@ export interface CreateGrantResponse { notification: NotifyOutcomeSet; } -export function roleFromPermissions(perms: Iterable): ShareRole { - const set = new Set(perms); - if (set.has('delete') || set.has('share')) return 'admin'; - if (set.has('create') || set.has('update')) return 'editor'; +/** + * Map a backend role string to the role the UI exposes. The server may emit the + * full enum (`owner`/`editor`/`viewer`/`commenter`/`contributor`); the picker + * only shows Owner/Editor/Viewer, so collapse the two unexposed roles to their + * closest neighbour rather than render an unknown option. + */ +export function displayRole(role: string | undefined): ShareRole { + if (role === 'owner' || role === 'editor' || role === 'viewer') return role; + if (role === 'contributor') return 'editor'; + if (role === 'commenter') return 'viewer'; return 'viewer'; } diff --git a/frontend/src/lib/components/ShareDialog.svelte b/frontend/src/lib/components/ShareDialog.svelte index a054e141..8dbd6403 100644 --- a/frontend/src/lib/components/ShareDialog.svelte +++ b/frontend/src/lib/components/ShareDialog.svelte @@ -10,10 +10,10 @@ import { createGrant, expiryToIso, + displayRole, fetchGrantsForResource, notifyGrantRecipient, revokeGrant, - roleFromPermissions, updateGrantRole, type Grant, type GrantSubject, @@ -51,11 +51,11 @@ let directoryAvailable = $state(true); const ROLES: { v: ShareRole; l: string; icon: string }[] = [ - { v: 'admin', l: t('share.role.canManage', 'Can manage'), icon: 'crown' }, + { v: 'owner', l: t('share.role.canManage', 'Can manage'), icon: 'crown' }, { v: 'editor', l: t('share.role.canEdit', 'Can edit'), icon: 'pencil-alt' }, { v: 'viewer', l: t('share.role.canView', 'Can view'), icon: 'eye' } ]; - const ROLE_ORDER: ShareRole[] = ['admin', 'editor', 'viewer']; + const ROLE_ORDER: ShareRole[] = ['owner', 'editor', 'viewer']; function roleLabel(r: ShareRole): string { return ROLES.find((x) => x.v === r)?.l ?? r; } @@ -89,13 +89,20 @@ function groupGrants(grants: Grant[]): Member[] { const bySubject = new Map< string, - { subject: GrantSubject; perms: string[]; ids: string[]; expiry: string | null } + { subject: GrantSubject; role: ShareRole; ids: string[]; expiry: string | null } >(); for (const g of grants) { if (g.subject.type === 'token') continue; const key = `${g.subject.type}:${g.subject.id}`; - const entry = bySubject.get(key) ?? { subject: g.subject, perms: [], ids: [], expiry: null }; - entry.perms.push(g.permission); + const entry = bySubject.get(key) ?? { + subject: g.subject, + role: 'viewer' as ShareRole, + ids: [], + expiry: null + }; + // Role-grants emit one row per (subject, resource), so the row's role + // is the subject's role directly. + entry.role = displayRole(g.role); entry.ids.push(g.id); if (g.expires_at && !entry.expiry) entry.expiry = isoToDate(g.expires_at); bySubject.set(key, entry); @@ -103,7 +110,7 @@ return [...bySubject.values()].map((e) => ({ subject: e.subject, recipient: resolveRecipient(e.subject.type as 'user' | 'group', e.subject.id), - role: roleFromPermissions(e.perms), + role: e.role, grantIds: e.ids, notifyGrantId: e.ids[0], expiry: e.expiry, diff --git a/frontend/src/routes/shared/+page.svelte b/frontend/src/routes/shared/+page.svelte index a86ba371..d333ecb7 100644 --- a/frontend/src/routes/shared/+page.svelte +++ b/frontend/src/routes/shared/+page.svelte @@ -4,6 +4,7 @@ import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import { + displayRole, expiryToIso, fetchMyShares, notifyGrantRecipient, @@ -33,12 +34,12 @@ ]; const ROLES: { v: ShareRole; l: string; icon: string }[] = [ - { v: 'admin', l: t('share.role.canManage', 'Can manage'), icon: 'crown' }, + { v: 'owner', l: t('share.role.canManage', 'Can manage'), icon: 'crown' }, { v: 'editor', l: t('share.role.canEdit', 'Can edit'), icon: 'pencil-alt' }, { v: 'viewer', l: t('share.role.canView', 'Can view'), icon: 'eye' } ]; function roleMeta(r: string) { - return ROLES.find((x) => x.v === r) ?? ROLES[2]; + return ROLES.find((x) => x.v === displayRole(r)) ?? ROLES[2]; } let raw = $state([]);