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:
@@ -31,6 +31,7 @@
|
||||
import type { ItemType, ShareItem } from '$lib/api/types';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import UserVignette from '$lib/components/UserVignette.svelte';
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { ui } from '$lib/stores/ui.svelte';
|
||||
|
||||
@@ -74,7 +75,6 @@
|
||||
/** Representative grant id for notify (any grant on this subject). */
|
||||
notifyGrantId?: string;
|
||||
expiry: string | null; // YYYY-MM-DD or null
|
||||
isExternal: boolean;
|
||||
}
|
||||
let members = $state<Member[]>([]);
|
||||
let grantsLoading = $state(false);
|
||||
@@ -115,8 +115,7 @@
|
||||
role: e.role,
|
||||
grantIds: e.ids,
|
||||
notifyGrantId: e.ids[0],
|
||||
expiry: e.expiry,
|
||||
isExternal: false
|
||||
expiry: e.expiry
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -452,12 +451,20 @@
|
||||
class="member"
|
||||
class:member--expired={m.expiry && new Date(m.expiry) < new Date()}
|
||||
>
|
||||
<Icon name={m.subject.type === 'group' ? 'user-group' : 'user'} />
|
||||
<span class="member__label">
|
||||
{m.recipient.label}
|
||||
{#if m.recipient.sublabel}<span class="member__sub">{m.recipient.sublabel}</span
|
||||
>{/if}
|
||||
</span>
|
||||
{#if m.subject.type === 'user'}
|
||||
<UserVignette
|
||||
userId={m.subject.id}
|
||||
fallbackLabel={m.recipient.label}
|
||||
fallbackSublabel={m.recipient.sublabel}
|
||||
/>
|
||||
{:else}
|
||||
<Icon name="user-group" />
|
||||
<span class="member__label">
|
||||
{m.recipient.label}
|
||||
{#if m.recipient.sublabel}<span class="member__sub">{m.recipient.sublabel}</span
|
||||
>{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{@render expiryChip(m.expiry, (v) => changeMemberExpiry(m, v))}
|
||||
<select
|
||||
class="role-select"
|
||||
|
||||
Reference in New Issue
Block a user