fix(02): show owner name / Family in event popover footer

Backend:
- expand.ts: add ownerName: string | null to CalendarOccurrence
  interface and expandOccurrences() signature; thread it onto every
  emitted occurrence.
- events.ts: SELECT users.displayName as ownerName in the join; pass
  it to expandOccurrences().

Frontend:
- client.ts: add ownerName: string | null to CalendarOccurrence.
- EventDetailPopover.tsx: render isShared ? 'Family' :
  (ownerName ?? calendarName) in the footer instead of calendarName.

Tests:
- expand.test.ts: pass ownerName to all expandOccurrences() calls;
  assert ownerName is carried onto occurrences in the DST test.
- events.test.ts: add ownerName to mock rows; assert ownerName present
  on occurrences; add ownerName assertion to timed-recurring test.
- EventDetailPopover.test.tsx: add ownerName to fixtures; split
  "calendar name in footer" into three targeted tests covering
  personal-with-owner, shared→Family, and null-owner fallback.
This commit is contained in:
Lucas Berger
2026-06-05 15:14:43 -04:00
parent fc758e8ea6
commit 194f6a82a8
7 changed files with 64 additions and 7 deletions
+6
View File
@@ -57,6 +57,12 @@ export interface CalendarOccurrence {
calendarId: number // DB calendar-row id — do NOT use for SX calendarId routing
calendarName: string
ownerUserId: number // DB user id — the correct Schedule-X routing key
/**
* Display name of the calendar owner (users.displayName from the API).
* Null when the user has not configured a display name.
* Popover renders: isShared ? 'Family' : (ownerName ?? calendarName)
*/
ownerName: string | null
color: string // hex from users.color or shared-family constant
isShared: boolean // true → 'shared' slot; false → String(ownerUserId) slot
title: string
@@ -39,6 +39,7 @@ const TIMED_OCCURRENCE: CalendarOccurrence = {
calendarId: 1,
calendarName: 'My Calendar',
ownerUserId: 1,
ownerName: 'Alice',
color: '#4A90D9',
isShared: false,
title: 'Team Standup',
@@ -64,6 +65,7 @@ const ALLDAY_OCCURRENCE: CalendarOccurrence = {
calendarId: 2,
calendarName: 'Shared Calendar',
ownerUserId: 1,
ownerName: 'Alice',
color: '#F25C7A',
isShared: true,
title: 'Birthday Party',
@@ -124,8 +126,25 @@ describe('EventDetailPopover', () => {
expect(screen.getByText(/Daily team sync meeting/)).toBeDefined()
})
it('renders calendar name in footer', () => {
it('renders owner name in footer for personal events', () => {
renderPopover(TIMED_OCCURRENCE)
// TIMED_OCCURRENCE is personal (isShared:false) with ownerName:'Alice'
expect(screen.getByText(/Alice/)).toBeDefined()
})
it('renders "Family" in footer for shared calendar events', () => {
renderPopover(ALLDAY_OCCURRENCE)
// ALLDAY_OCCURRENCE has isShared:true — footer must show 'Family'
expect(screen.getByText('Family')).toBeDefined()
})
it('renders calendarName in footer when ownerName is null', () => {
const noOwnerName: CalendarOccurrence = {
...TIMED_OCCURRENCE,
id: 'no-owner-uid::2026-06-15T10:00:00',
ownerName: null,
}
renderPopover(noOwnerName)
expect(screen.getByText(/My Calendar/)).toBeDefined()
})
@@ -369,8 +369,12 @@ export function EventDetailPopover(props: ScheduleXEventModalProps = {}) {
}}
aria-hidden="true"
/>
{/* Plain text — XSS guard */}
{occurrence.calendarName}
{/* Plain text — XSS guard (T-02e-01).
Show 'Family' for shared-calendar events; owner display name for personal
events; fall back to calendarName when ownerName is null. */}
{occurrence.isShared
? 'Family'
: (occurrence.ownerName ?? occurrence.calendarName)}
</div>
{/* Phase 3 footer action area — Phase 3 adds edit/delete actions here (D-08) */}