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
+36 -100
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { pageTitle } from '#lib/branding.js';
import { Disc3, Album, Music2, Check, X, SlidersHorizontal } from 'lucide-svelte';
import { Check, X, SlidersHorizontal } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import RowActionsMenu, { type RowAction } from '#lib/components/RowActionsMenu.svelte';
import TabStrip from '#lib/components/TabStrip.svelte';
@@ -9,9 +8,9 @@
import { createReleaseProfilesQuery } from '#lib/api/wanted.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import StatusPill from '#lib/components/StatusPill.svelte';
import RequestRow, { requestName } from '#lib/components/RequestRow.svelte';
import Modal from '#lib/components/Modal.svelte';
import type { MusicRequest, RequestKind, RequestStatus } from '#lib/api/types.js';
import type { MusicRequest, RequestStatus } from '#lib/api/types.js';
// Approval queue. Tabs filter by status; rows expose Approve/Reject, and an
// artist request can be approved on a chosen release-type profile. Approving
@@ -50,24 +49,6 @@
let rejectOpen = $state<string | null>(null);
let rejectNotes = $state<string>('');
function fallbackIcon(kind: RequestKind) {
if (kind === 'artist') return Disc3;
if (kind === 'album') return Album;
return Music2;
}
function rowTitle(r: MusicRequest): string {
if (r.kind === 'artist') return r.artist_name;
if (r.kind === 'album') return r.album_title ?? '—';
return r.track_title ?? '—';
}
function rowAccessibleName(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}`;
}
// The id is a UUID — first 8 chars is plenty to disambiguate without a real
// username, and matches what the polish pass will replace with the real
// username surface.
@@ -174,38 +155,8 @@
{:else}
<ul class="divide-y divide-border rounded-lg border border-border bg-surface">
{#each rows as r (r.id)}
{@const Icon = fallbackIcon(r.kind)}
<li class="flex items-start gap-4 p-3" data-testid="admin-request-row" 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">
<span class="kind-pill">{r.kind}</span>
<StatusPill status={r.status} />
</div>
<div class="truncate text-base font-medium text-text-primary">
{rowTitle(r)}
</div>
<div class="truncate text-sm text-text-secondary">
{rowMeta(r)}
</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}
<RequestRow request={r} meta={rowMeta(r)} testid="admin-request-row">
{#snippet details()}
{#if r.kind === 'track' && r.album_title}
<div class="text-sm text-text-secondary" data-testid="track-disclosure">
Approving wants the album <em class="font-medium text-text-primary">{r.album_title}</em>.
@@ -214,39 +165,38 @@
{#if r.notes}
<div class="text-sm text-text-secondary">{r.notes}</div>
{/if}
</div>
{#if r.status === 'pending'}
{@const primary: RowAction = {
icon: Check,
label: 'Approve',
ariaLabel: `Approve ${rowAccessibleName(r)}`,
onclick: () => onApprove(r)
}}
{@const secondary: RowAction[] = [
...(r.kind === 'artist'
? [
{
icon: SlidersHorizontal,
label: 'Approve on a profile',
ariaLabel: `Approve ${rowAccessibleName(r)} on a profile`,
onclick: () => openProfile(r)
}
]
: []),
{
icon: X,
label: 'Reject',
ariaLabel: `Reject ${rowAccessibleName(r)}`,
onclick: () => openReject(r),
danger: true
}
]}
<div class="shrink-0">
{/snippet}
{#snippet actions()}
{#if r.status === 'pending'}
{@const primary: RowAction = {
icon: Check,
label: 'Approve',
ariaLabel: `Approve ${requestName(r)}`,
onclick: () => onApprove(r)
}}
{@const secondary: RowAction[] = [
...(r.kind === 'artist'
? [
{
icon: SlidersHorizontal,
label: 'Approve on a profile',
ariaLabel: `Approve ${requestName(r)} on a profile`,
onclick: () => openProfile(r)
}
]
: []),
{
icon: X,
label: 'Reject',
ariaLabel: `Reject ${requestName(r)}`,
onclick: () => openReject(r),
danger: true
}
]}
<RowActionsMenu {primary} {secondary} />
</div>
{/if}
</li>
{/if}
{/snippet}
</RequestRow>
{/each}
</ul>
{/if}
@@ -328,17 +278,3 @@
</button>
</div>
</Modal>
<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);
text-transform: capitalize;
}
</style>