fix(frontend): align sharing UI with the merged role-grants backend

The merge brought in main's ReBAC→role-grants migration, which changed the
grant contract the Svelte sharing UI (branched before it) was written
against: GrantDto dropped `permission` and now carries an explicit `role`
(owner/editor/viewer/commenter/contributor), and the "admin" role was
renamed "owner". Left unchanged, the share UI derived roles from a
now-absent `permission` field and showed every member as "viewer".

- grants.ts: ShareRole is now viewer|editor|owner; Grant carries `role`
  (not `permission`); `roleFromPermissions` → `displayRole`, which collapses
  the unexposed commenter→viewer and contributor→editor.
- ShareDialog.svelte: read each subject's role directly (role-grants emits
  one row per subject); role picker exposes Owner instead of Admin.
- shared/+page.svelte (My Shares): same owner rename; role badges run
  through displayRole so server-only roles render sensibly.

Create/update already POST `role`, so only the read/display path and the
role literal needed fixing. npm run check, test:unit (36) and build pass.
This commit is contained in:
Claude
2026-06-19 13:04:32 +00:00
parent 5494efea35
commit c2b53fcda9
3 changed files with 36 additions and 16 deletions
+14 -7
View File
@@ -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,