Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions suite/apps/tableaux/components/guestPanel/GuestPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import GuestPanel from './GuestPanel'
import { useStore } from '../../store/useStore'
import { openPlan } from '../../test/openPlan'
import { newGuest } from '@/lib/model/factories'
import type { Guest } from '../../store/types'

const guest = (id: string, fullName: string, where: Partial<Guest>): Guest =>
({ ...newGuest({ id, rsvpStatus: 'confirmed' }), fullName, assignedSeatId: null, ...where }) as Guest

// Families sit inside a subgroup inside a group, which is how a real plan is
// built. Searching any container's name should find the people in it.
beforeEach(() => {
openPlan({
guests: {
g1: guest('g1', 'Ada Lovelace', { groupId: 'grp', subgroupId: 'sub', familyId: 'fam' }),
g2: guest('g2', 'Byron Lovelace', { groupId: 'grp', subgroupId: 'sub', familyId: 'fam' }),
g3: guest('g3', 'Mary Somerville', { groupId: 'grp', subgroupId: 'sub' }),
g4: guest('g4', 'Alan Turing', { groupId: 'grp' }),
},
groups: { grp: { id: 'grp', name: 'Work', colour: '#000', memberIds: ['g1', 'g2', 'g3', 'g4'] } },
subgroups: { sub: { id: 'sub', name: 'Analysts', colour: '#000', parentGroupId: 'grp', memberIds: ['g1', 'g2', 'g3'] } },
families: {
fam: { id: 'fam', name: 'The Engines', colour: '#000', parentGroupId: null, parentSubgroupId: 'sub', memberIds: ['g1', 'g2'] },
},
})
})

const search = (query: string) => {
useStore.setState({ search: query })
render(<GuestPanel />)
}

describe('GuestPanel search', () => {
it("finds a family's members by the family's name", () => {
search('engines')
expect(screen.getByText('Ada Lovelace')).toBeInTheDocument()
expect(screen.getByText('Byron Lovelace')).toBeInTheDocument()
expect(screen.queryByText('Mary Somerville')).toBeNull()
expect(screen.queryByText('Alan Turing')).toBeNull()
})

it("finds a subgroup's members by the subgroup's name", () => {
search('analysts')
expect(screen.getByText('Ada Lovelace')).toBeInTheDocument()
expect(screen.getByText('Mary Somerville')).toBeInTheDocument()
expect(screen.queryByText('Alan Turing')).toBeNull()
})

it("still finds a group's members by the group's name", () => {
search('work')
expect(screen.getByText('Alan Turing')).toBeInTheDocument()
expect(screen.getByText('Ada Lovelace')).toBeInTheDocument()
})

it('still finds a person by their own name', () => {
search('turing')
expect(screen.getByText('Alan Turing')).toBeInTheDocument()
expect(screen.queryByText('Ada Lovelace')).toBeNull()
})
})
16 changes: 8 additions & 8 deletions suite/apps/tableaux/components/guestPanel/GuestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useGuestImport } from '@/components/shell/guestImportPanel'
import { matchesSearch, matchesFilters } from '../../utils/guestFilters'
import { isComing } from '@/lib/model/slices'
import type { ShownFamily, ShownGroup, ShownSubgroup } from './tree'
import type { Guest } from '../../store/types'
import styles from './GuestPanel.module.css'

/**
Expand Down Expand Up @@ -80,18 +81,17 @@ export default function GuestPanel() {
const subgroupArr = Object.values(subgroups)
const familyArr = Object.values(families)
const showEmpty = !search && !filters.length
const shown = (g: Guest) => {
const names = [groups[g.groupId ?? '']?.name, subgroups[g.subgroupId ?? '']?.name, families[g.familyId ?? '']?.name]
return matchesSearch(g, names, search) && matchesFilters(g, filters)
}

const vGroups = groupArr
.map((group): ShownGroup => {
// TODO(family-ux): matchesSearch is only ever given the top-level
// `group`, so searching a subgroup or family NAME doesn't surface its
// members (only a personal-name match does) — a real gap for nested
// families, which is how every family in the live plan is set up.
// See tmp/family-ux-followups.md #2.
const allMembers = (group.memberIds || [])
.map((id) => guests[id])
.filter(Boolean)
.filter((g) => matchesSearch(g, group, search) && matchesFilters(g, filters))
.filter(shown)

// Build subgroup sections: each subgroup with its matching members,
// and any families nested inside that subgroup.
Expand Down Expand Up @@ -155,15 +155,15 @@ export default function GuestPanel() {
members: (f.memberIds || [])
.map((id) => guests[id])
.filter(Boolean)
.filter((g) => matchesSearch(g, f, search) && matchesFilters(g, filters)),
.filter(shown),
}))
.filter((vf) => vf.members.length > 0 || showEmpty)
.sort((a, b) => String(a.family.name).localeCompare(String(b.family.name)))

const ung = list
.filter((g) => !g.groupId || !groups[g.groupId])
.filter((g) => !g.familyId || !families[g.familyId])
.filter((g) => matchesSearch(g, null, search) && matchesFilters(g, filters))
.filter(shown)
.sort((a, b) => String(a.fullName).localeCompare(String(b.fullName)))

return {
Expand Down
16 changes: 9 additions & 7 deletions suite/apps/tableaux/utils/guestFilters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sideShort } from '@/lib/model/partners'
import { isComing } from '@/lib/model/slices'
import type { Group, Guest, Meta } from '../store/types'
import type { Guest, Meta } from '../store/types'

// TODO(family-ux): no "in a family" (or per-family) filter chip exists —
// would need an entry here AND in PREDICATES below, and a predicate can't
Expand Down Expand Up @@ -48,15 +48,17 @@ export function matchesFilters(guest: Guest, filters: readonly string[] | null |
return filters.every((f) => (f in PREDICATES ? PREDICATES[f as FilterKey](guest) : true))
}

export function matchesSearch(guest: Guest, group: Pick<Group, 'name'> | null | undefined, query: string): boolean {
/**
* Does the guest match the search box? Their own names count, and so do the
* names of the group, subgroup and family they are in, so searching "The
* Engines" finds the Engines.
*/
export function matchesSearch(guest: Guest, containerNames: readonly (string | undefined)[], query: string): boolean {
if (!query) return true
const q = query.trim().toLowerCase()
if (!q) return true
return (
(guest.fullName || '').toLowerCase().includes(q) ||
(guest.firstName || '').toLowerCase().includes(q) ||
(guest.lastName || '').toLowerCase().includes(q) ||
(group?.name || '').toLowerCase().includes(q)
return [guest.fullName, guest.firstName, guest.lastName, ...containerNames].some((name) =>
(name || '').toLowerCase().includes(q)
)
}

Expand Down
Loading