Fix preferred player name resolution
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-20 17:15:42 +03:00
parent fa4edc0df5
commit ffa50f20a7
3 changed files with 15 additions and 6 deletions

View File

@@ -100,6 +100,14 @@ describe('Mixmaker frontend', () => {
expect(screen.getByRole('textbox', { name: 'Discord invite URL' })).toHaveValue('https://discord.gg/mixmaker')
})
it('resolves preferred player names outside the event registrations', async () => {
window.history.replaceState({}, '', '/admin')
render(<LanguageProvider><App /></LanguageProvider>)
fireEvent.click(await screen.findByRole('tab', { name: 'Participants' }))
expect(await screen.findByText(/Preferred with Zenith/)).toBeInTheDocument()
expect(screen.queryByText(/Preferred with p12/)).not.toBeInTheDocument()
})
it('edits player identity', async () => {
window.history.replaceState({}, '', '/profile')
render(<LanguageProvider><App /></LanguageProvider>)

View File

@@ -321,7 +321,7 @@ function ProfilePage() {
const language = useLanguage()
const options = useMemo(() => rankOptions(language), [language])
const profile = useQuery({ queryKey: ['profile'], queryFn: api.profile, initialData: demoMode ? demoProfile : undefined })
const players = useQuery({ queryKey: ['players'], queryFn: api.players, initialData: demoMode ? demoRegistrations.map((registration) => registration.player) : undefined })
const players = useQuery({ queryKey: ['players'], queryFn: api.players, initialData: demoMode ? demoPlayerProfiles : undefined })
const [ratings, setRatings] = useState<Record<PlayerRole, RankOrdinal>>({ tank: 20, damage: 20, support: 20 })
const [displayName, setDisplayName] = useState('')
const [battleTag, setBattleTag] = useState('')
@@ -498,6 +498,7 @@ function Participants({ eventId }: { eventId: string }) {
const [status, setStatus] = useState<RsvpStatus>('going')
const [ratings, setRatings] = useState<Record<PlayerRole, RankOrdinal>>({ tank: 13, damage: 13, support: 13 })
const rows = useQuery({ queryKey: ['registrations', eventId], queryFn: () => api.registrations(eventId), initialData: demoMode ? demoRegistrations : undefined })
const players = useQuery({ queryKey: ['players'], queryFn: api.players, initialData: demoMode ? demoPlayerProfiles : undefined })
const create = useMutation({
mutationFn: () => {
if (!demoMode) return api.createParticipant(eventId, displayName.trim(), ratings, status)
@@ -540,9 +541,9 @@ function Participants({ eventId }: { eventId: string }) {
if (!demoMode) void client.invalidateQueries({ queryKey: ['events'] })
},
})
if (rows.isLoading) return <LoadingState label="Loading participants…" />
if (rows.isError || !rows.data) return <ErrorState retry={() => void rows.refetch()} />
const names = new Map(rows.data.map((registration) => [registration.player.id, registration.player.displayName]))
if (rows.isLoading || players.isLoading) return <LoadingState label="Loading participants…" />
if (rows.isError || players.isError || !rows.data || !players.data) return <ErrorState retry={() => { void rows.refetch(); void players.refetch() }} />
const names = new Map(players.data.map((player) => [player.id, player.displayName]))
return <section className="card table-card"><form className="guest-participant-form" onSubmit={(event) => { event.preventDefault(); create.mutate() }}><div><h2>Add participant</h2><p>Create a guest player when they cannot sign in with Discord.</p></div><div className="guest-participant-fields"><label>Nickname<input required maxLength={80} value={displayName} onChange={(event) => setDisplayName(event.target.value)} /></label>{(['tank', 'damage', 'support'] as PlayerRole[]).map((role) => <label key={role}>{role === 'tank' ? 'Tank rank' : role === 'damage' ? 'Damage rank' : 'Support rank'}<select value={ratings[role]} onChange={(event) => setRatings((current) => ({ ...current, [role]: toRankOrdinal(Number(event.target.value)) }))}>{options.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>)}<label>Status<select value={status} onChange={(event) => setStatus(event.target.value as RsvpStatus)}><option value="going">Going</option><option value="maybe">Maybe</option><option value="not_going">Not going</option></select></label><button className="button primary" disabled={create.isPending || !displayName.trim()}>{create.isPending ? 'Adding…' : 'Add participant'}</button></div>{create.isError && <p className="error-note">{create.error.message}</p>}</form><div className="section-title"><div><h2>Participant responses</h2><p>Admin overrides are attributed and audited.</p></div><div className="count-pills"><Badge tone="success">{rows.data.filter((r) => r.status === 'going').length} going</Badge><Badge tone="warning">{rows.data.filter((r) => r.status === 'maybe').length} maybe</Badge></div></div><div className="participant-list">{rows.data.map((r) => <div className="participant-row" key={r.id}><div className="mini-avatar">{r.player.displayName.slice(0, 2).toUpperCase()}</div><div className="participant-name"><PlayerLink player={r.player} /><span>{r.player.preferredRoles.map((role) => role[0].toUpperCase() + role.slice(1)).join(' · ') || 'No preferred roles'}{r.player.preferredPlayerIds.length > 0 && <> · Preferred with {r.player.preferredPlayerIds.map((id) => names.get(id) ?? id.slice(0, 6)).join(', ')}</>}{r.changedBy?.source === 'admin' && <> · <em>set by {r.changedBy.displayName}</em></>}</span></div><div className="mini-ratings"><span>T · {rankLabel(r.player.ratings.tank, language)}</span><span>D · {rankLabel(r.player.ratings.damage, language)}</span><span>S · {rankLabel(r.player.ratings.support, language)}</span></div><select disabled={override.isPending || remove.isPending} aria-label={`RSVP for ${r.player.displayName}`} value={r.status} onChange={(e) => override.mutate({ playerId: r.player.id, status: e.target.value as RsvpStatus })}><option value="going">Going</option><option value="maybe">Maybe</option><option value="not_going">Not going</option></select><button className="icon-button remove-participant" type="button" disabled={remove.isPending} aria-label={`Remove ${r.player.displayName} from event`} title="Remove from event" onClick={() => remove.mutate(r.player.id)}><X /></button></div>)}</div>{(override.isError || remove.isError) && <p className="error-note">{override.error?.message ?? remove.error?.message}</p>}</section>
}

View File

@@ -150,11 +150,11 @@ const players = [
makePlayer('p11', 'Echo', 26, 32, 31),
makePlayer('p12', 'Zenith', 28, 30, 35),
]
players[1].preferredPlayerIds = ['p1']
players[1].preferredPlayerIds = ['p12']
players[4].preferredPlayerIds = ['p1']
players[0].avoidedPlayerIds = ['p3']
export const demoRegistrations: Registration[] = players.map((player, index) => ({
export const demoRegistrations: Registration[] = players.slice(0, -1).map((player, index) => ({
id: `reg-${index + 1}`,
player,
status: index < 9 ? 'going' : index < 11 ? 'maybe' : 'not_going',