Fix share name toggle revert and stale table data for umbral contacts

Bug 1: _to_settings_response() was missing share_first_name and
share_last_name — the response always returned false (Pydantic default),
causing the frontend to sync toggles back to off after save.

Bug 2: Table column renderers read from stale Person record fields.
Added sf() helper that overlays shared_fields for umbral contacts,
applied to name, phone, email, role, and birthday columns. The table
now shows live shared profile data matching the detail panel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-04 08:07:45 +08:00
parent 33aac72639
commit 4513227338
2 changed files with 34 additions and 16 deletions

View File

@ -48,6 +48,8 @@ def _to_settings_response(s: Settings) -> SettingsResponse:
# Social settings # Social settings
accept_connections=s.accept_connections, accept_connections=s.accept_connections,
# Sharing defaults # Sharing defaults
share_first_name=s.share_first_name,
share_last_name=s.share_last_name,
share_preferred_name=s.share_preferred_name, share_preferred_name=s.share_preferred_name,
share_email=s.share_email, share_email=s.share_email,
share_phone=s.share_phone, share_phone=s.share_phone,

View File

@ -85,6 +85,14 @@ function sortPeople(people: Person[], key: string, dir: 'asc' | 'desc'): Person[
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Column definitions // Column definitions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** Get a field value, preferring shared_fields for umbral contacts. */
function sf(p: Person, key: string): string | null | undefined {
if (p.is_umbral_contact && p.shared_fields && key in p.shared_fields) {
return p.shared_fields[key] as string | null;
}
return p[key as keyof Person] as string | null | undefined;
}
const columns: ColumnDef<Person>[] = [ const columns: ColumnDef<Person>[] = [
{ {
key: 'name', key: 'name',
@ -92,7 +100,10 @@ const columns: ColumnDef<Person>[] = [
sortable: true, sortable: true,
visibilityLevel: 'essential', visibilityLevel: 'essential',
render: (p) => { render: (p) => {
const initialsName = getPersonInitialsName(p); const firstName = sf(p, 'first_name') ?? p.first_name;
const lastName = sf(p, 'last_name') ?? p.last_name;
const liveName = [firstName, lastName].filter(Boolean).join(' ') || p.nickname || p.name;
const initialsName = liveName || getPersonInitialsName(p);
return ( return (
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div <div
@ -100,7 +111,7 @@ const columns: ColumnDef<Person>[] = [
> >
{getInitials(initialsName)} {getInitials(initialsName)}
</div> </div>
<span className="font-medium truncate">{p.nickname || p.name}</span> <span className="font-medium truncate">{liveName}</span>
{p.is_umbral_contact && ( {p.is_umbral_contact && (
<Ghost className="h-3.5 w-3.5 text-violet-400 shrink-0" aria-label="Umbral contact" /> <Ghost className="h-3.5 w-3.5 text-violet-400 shrink-0" aria-label="Umbral contact" />
)} )}
@ -113,18 +124,21 @@ const columns: ColumnDef<Person>[] = [
label: 'Number', label: 'Number',
sortable: false, sortable: false,
visibilityLevel: 'essential', visibilityLevel: 'essential',
render: (p) => ( render: (p) => {
<span className="text-muted-foreground truncate">{p.mobile || p.phone || '—'}</span> const mobile = sf(p, 'mobile') ?? p.mobile;
), const phone = sf(p, 'phone') ?? p.phone;
return <span className="text-muted-foreground truncate">{mobile || phone || '—'}</span>;
},
}, },
{ {
key: 'email', key: 'email',
label: 'Email', label: 'Email',
sortable: true, sortable: true,
visibilityLevel: 'essential', visibilityLevel: 'essential',
render: (p) => ( render: (p) => {
<span className="text-muted-foreground truncate">{p.email || '—'}</span> const email = sf(p, 'email') ?? p.email;
), return <span className="text-muted-foreground truncate">{email || '—'}</span>;
},
}, },
{ {
key: 'job_title', key: 'job_title',
@ -132,10 +146,10 @@ const columns: ColumnDef<Person>[] = [
sortable: true, sortable: true,
visibilityLevel: 'filtered', visibilityLevel: 'filtered',
render: (p) => { render: (p) => {
const parts = [p.job_title, p.company].filter(Boolean); const jobTitle = sf(p, 'job_title') ?? p.job_title;
return ( const company = sf(p, 'company') ?? p.company;
<span className="text-muted-foreground truncate">{parts.join(', ') || '—'}</span> const parts = [jobTitle, company].filter(Boolean);
); return <span className="text-muted-foreground truncate">{parts.join(', ') || '—'}</span>;
}, },
}, },
{ {
@ -143,12 +157,14 @@ const columns: ColumnDef<Person>[] = [
label: 'Birthday', label: 'Birthday',
sortable: true, sortable: true,
visibilityLevel: 'filtered', visibilityLevel: 'filtered',
render: (p) => render: (p) => {
p.birthday ? ( const birthday = sf(p, 'birthday') ?? p.birthday;
<span className="text-muted-foreground">{format(parseISO(p.birthday), 'MMM d')}</span> return birthday ? (
<span className="text-muted-foreground">{format(parseISO(birthday), 'MMM d')}</span>
) : ( ) : (
<span className="text-muted-foreground"></span> <span className="text-muted-foreground"></span>
), );
},
}, },
{ {
key: 'category', key: 'category',