'use client' /** * MembersTable — headless members list. Renders avatar + name + email + * role badge + actions per row. When `canManage` is true the row shows a * RoleSelector (calls onChangeRole) + a Remove button (calls onRemove). * * Pagination is intentionally absent — the backend endpoint at * /api/v1/teams/{id}/members returns the full list. SSP support is a * follow-up bead. startsim-o7s. */ import * as React from 'react' import { RoleSelector } from './RoleSelector' import { userDisplayName, userInitials, type MemberRow, type TeamRole, } from './types' import { MEMBERS_TABLE_DEFAULTS, type MembersTableClassNames, } from './members-table-default-class-names' export interface MembersTableProps { members: MemberRow[] /** * When true, expose the role selector + remove button. Apps should pass * `canManage = isAdmin || isOwner` derived from useMembership(). */ canManage?: boolean onChangeRole?: (userId: string, role: TeamRole) => void | Promise onRemove?: (userId: string) => void | Promise /** Custom empty-state slot. Defaults to a generic "No members yet" cell. */ emptyState?: React.ReactNode classNames?: MembersTableClassNames /** Restrict role-selector options (e.g. hide 'owner' when caller isn't owner). */ availableRoles?: TeamRole[] /** * The signed-in user's id. When it matches a row, that row is badged "You" — * otherwise the owner is an anonymous line sitting next to a live Remove * button with nothing marking it as themselves. */ currentUserId?: string } const ROLE_LABEL: Record = { owner: 'Owner', admin: 'Admin', member: 'Member', viewer: 'Viewer', } export function MembersTable({ members, canManage = false, onChangeRole, onRemove, emptyState, classNames, availableRoles, currentUserId, }: MembersTableProps) { const cls = { ...MEMBERS_TABLE_DEFAULTS, ...(classNames ?? {}) } // Role + actions hug their content so the Member column takes the slack — // one shared cell class used to split the table down the middle and leave // the controls floating in a gap. const tight = (base: string, extra: string) => `${base} ${extra}`.trim() return (
{canManage && ( {members.length === 0 && ( )} {members.map((m) => { const displayName = userDisplayName(m.user) || m.userId // Members with no name fall back to their email, so printing the // email underneath stacked the same string on itself. const secondary = m.user?.email && m.user.email !== displayName ? m.user.email : null const isSelf = !!currentUserId && m.userId === currentUserId return ( {canManage && ( )} ) })}
Member Role )}
{emptyState ?? 'No members yet.'}
{m.user?.avatarUrl ? ( ) : ( )}
{displayName} {isSelf && You} {secondary && {secondary}}
{canManage && onChangeRole ? ( onChangeRole(m.userId, role)} availableRoles={availableRoles} /> ) : ( {ROLE_LABEL[m.role]} )}
{onRemove && ( )}
) }