feat(shares): show external users with avatar, email and a badge (#500)
Two related parity gaps from the VanillaJS → Svelte migration (issue #500): internal-vs-external users weren't badged, and external users in a share's member list rendered as a bare UUID with a static icon — no avatar, no email. Both share one root cause: there was no shared user vignette and no resolver for non-directory (external) users (the system address book lists internal users only, and ShareDialog hardcoded isExternal=false). - lib/api/endpoints/users.ts: resolveUser(id) — cached GET /api/users/{id} (the authenticated per-user profile lookup) → {name, email, image, isExternal}; returns null when the profile isn't visible so callers keep their fallback label. - lib/components/UserVignette.svelte: reusable identity chip — avatar (photo or coloured initials), name, email, and a building-circle-xmark badge for external users; resolves lazily and falls back to a caller-supplied label. - lib/utils/avatar.ts: userInitials() + avatarColorIndex() extracted from AppShell (now shared by both — no duplicated logic) so vignette and account button render identically. - ShareDialog: user member rows now render <UserVignette>; groups keep their icon+label. Drops the dead hardcoded isExternal. Backend already exposes everything (UserDto.email/image/is_external via GET /api/users/{id}); no backend change. Frontend gate green (svelte-check 0/0, eslint, stylelint, prettier) + 47 Vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Per-user profile resolution via `GET /api/users/{id}`, cached per id.
|
||||
*
|
||||
* Used to render external (and any non-directory) users in share/recipient UIs
|
||||
* with their real name, email, avatar and an internal/external flag — the
|
||||
* system address book only lists internal users, so external grant subjects
|
||||
* would otherwise show as a bare UUID. Mirrors the original `systemUsers`
|
||||
* resolver. The endpoint enforces its own visibility rules; a non-visible
|
||||
* profile resolves to `null` so callers fall back to whatever label they have.
|
||||
*/
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
|
||||
export interface ResolvedUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string | null;
|
||||
isExternal: boolean;
|
||||
}
|
||||
|
||||
/** Subset of the backend `UserDto` we consume here. */
|
||||
interface UserDtoShape {
|
||||
id: string;
|
||||
username?: string | null;
|
||||
email?: string | null;
|
||||
image?: string | null;
|
||||
is_external: boolean;
|
||||
}
|
||||
|
||||
// id → in-flight/resolved lookup (the Promise is cached so concurrent callers
|
||||
// for the same id share one request, and a `null` result isn't re-fetched).
|
||||
const cache = new Map<string, Promise<ResolvedUser | null>>();
|
||||
|
||||
export function resolveUser(id: string): Promise<ResolvedUser | null> {
|
||||
const hit = cache.get(id);
|
||||
if (hit) return hit;
|
||||
|
||||
const pending = (async (): Promise<ResolvedUser | null> => {
|
||||
try {
|
||||
const res = await apiFetch(`/api/users/${encodeURIComponent(id)}`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const u = (await res.json()) as UserDtoShape;
|
||||
return {
|
||||
id: u.id,
|
||||
name: u.username?.trim() || u.email || u.id,
|
||||
email: u.email ?? '',
|
||||
image: u.image ?? null,
|
||||
isExternal: u.is_external
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
cache.set(id, pending);
|
||||
return pending;
|
||||
}
|
||||
Reference in New Issue
Block a user