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:
@@ -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<string>): 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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<OutgoingGrantItem[]>([]);
|
||||
|
||||
Reference in New Issue
Block a user