refactor: the admin and user Requests pages draw a request with one RequestRow, and the kind chip is one KindPill there and on Hidden tracks (#5651)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m16s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m10s
release / android (push) Successful in 5m17s
release / Build signed APK (releases and dev) (push) Successful in 5m33s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:20:53 -04:00
co-authored by Claude Opus 5.5
parent db15713798
commit 453a977e48
6 changed files with 206 additions and 192 deletions
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import type { Snippet } from 'svelte';
// A small accent-tinted chip naming what something is: a request's kind,
// a hidden track's reason. Beside StatusPill, which says where it stands.
let { children }: { children: Snippet } = $props();
</script>
<span class="kind-pill">{@render children()}</span>
<style>
.kind-pill {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
line-height: 14px;
background: color-mix(in srgb, var(--fs-accent) 15%, transparent);
color: var(--fs-accent-fg);
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<script lang="ts" module>
import type { MusicRequest } from '#lib/api/types.js';
// A request's name for aria-labels: '—' means nothing to a screen reader,
// so a missing title falls back to a phrase that still says what it is.
export function requestName(r: MusicRequest): string {
if (r.kind === 'artist') return r.artist_name;
if (r.kind === 'album') return r.album_title ?? `this album by ${r.artist_name}`;
return r.track_title ?? `this track by ${r.artist_name}`;
}
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
import { Disc3, Album, Music2 } from 'lucide-svelte';
import StatusPill from '#lib/components/StatusPill.svelte';
import KindPill from '#lib/components/KindPill.svelte';
import { plural } from '#lib/utils/plural.js';
// One request in a list, as the requester's page and the admin queue both
// show it: what was asked for, where it stands, and how much of it has
// arrived. Each page supplies who or when (meta), its own extra lines
// (details) and its own controls (actions).
let {
request: r,
meta,
testid,
details,
actions
}: {
request: MusicRequest;
meta: string;
testid: string;
details?: Snippet;
actions?: Snippet;
} = $props();
const Icon = $derived(r.kind === 'artist' ? Disc3 : r.kind === 'album' ? Album : Music2);
const title = $derived(
r.kind === 'artist' ? r.artist_name : r.kind === 'album' ? (r.album_title ?? '—') : (r.track_title ?? '—')
);
</script>
<li class="flex items-start gap-4 p-3" data-testid={testid} data-status={r.status}>
<div class="flex h-14 w-14 shrink-0 items-center justify-center rounded-md bg-surface-hover" aria-hidden="true">
<Icon size={24} strokeWidth={1} class="text-text-muted" />
</div>
<div class="min-w-0 flex-1 space-y-1">
<div class="flex flex-wrap items-center gap-2">
<KindPill>{r.kind[0].toUpperCase() + r.kind.slice(1)}</KindPill>
<StatusPill status={r.status} />
</div>
<div class="truncate text-base font-medium text-text-primary">{title}</div>
<div class="truncate text-sm text-text-secondary">{meta}</div>
{#if r.imported_album_count > 0 || r.imported_track_count > 0}
<div class="text-sm text-accent-fg" data-testid="ingest-progress">
{#if r.kind === 'artist'}
{plural(r.imported_album_count, 'album', 'albums')}
· {plural(r.imported_track_count, 'track', 'tracks')} ingested
{:else if r.kind === 'album'}
{plural(r.imported_track_count, 'track', 'tracks')} ingested
{:else}
Track ingested
{/if}
</div>
{/if}
{@render details?.()}
</div>
{#if actions}
<div class="flex shrink-0 items-center gap-2">{@render actions()}</div>
{/if}
</li>
+60
View File
@@ -0,0 +1,60 @@
import { describe, expect, test } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import type { MusicRequest } from '#lib/api/types.js';
import RequestRow, { requestName } from './RequestRow.svelte';
function req(over: Partial<MusicRequest> = {}): MusicRequest {
return {
id: 'r1',
user_id: 'u1',
status: 'approved',
kind: 'album',
artist_mbid: 'art-mbid',
artist_name: 'Boards of Canada',
album_title: 'Geogaddi',
track_title: null,
requested_at: '2026-04-28T12:00:00Z',
updated_at: '2026-04-28T12:00:00Z',
imported_album_count: 0,
imported_track_count: 0,
...over
};
}
const text = (el: HTMLElement) => (el.textContent ?? '').replace(/\s+/g, ' ').trim();
// The row both Requests pages draw (#5651): the admin queue and the
// requester's own list read the same request the same way.
describe('RequestRow', () => {
test('says what was asked for, where it stands, and who or when', () => {
render(RequestRow, { request: req(), meta: 'by Boards of Canada · 4/28/2026', testid: 'request-row' });
const row = screen.getByTestId('request-row');
expect(row.dataset.status).toBe('approved');
expect(text(row)).toMatch(/^Album Approved · downloading Geogaddi by Boards of Canada · 4\/28\/2026$/);
});
test('says how much has arrived, in the words for each kind', () => {
const { unmount } = render(RequestRow, {
request: req({ kind: 'artist', imported_album_count: 3, imported_track_count: 41 }),
meta: '',
testid: 'r'
});
expect(text(screen.getByTestId('ingest-progress'))).toBe('3 albums · 41 tracks ingested');
unmount();
render(RequestRow, { request: req({ kind: 'track', track_title: 'Roygbiv', imported_track_count: 1 }), meta: '', testid: 'r' });
expect(text(screen.getByTestId('ingest-progress'))).toBe('Track ingested');
});
test('nothing arrived yet says nothing about it', () => {
render(RequestRow, { request: req(), meta: '', testid: 'r' });
expect(screen.queryByTestId('ingest-progress')).toBeNull();
});
test('a request with no title still has a name a screen reader can say', () => {
render(RequestRow, { request: req({ album_title: null }), meta: '', testid: 'r' });
expect(text(screen.getByTestId('r'))).toMatch(/—/);
expect(requestName(req({ album_title: null }))).toBe('this album by Boards of Canada');
expect(requestName(req({ kind: 'artist' }))).toBe('Boards of Canada');
});
});