From 3df5e4b76d8731d191fc646111122c4dcac095d9 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sat, 2 May 2026 16:58:33 +0300 Subject: [PATCH] Match the legacy issue modal - Restore the shell-era issue detail layout and hero ordering. - Keep external links color-coded by service. - Hide track details for album issues and keep the track list compact. - Restore legacy track-list badge colors for format and bitrate. - Match the neutral dismiss button styling from the old modal. - Add regression coverage for the album issue modal state. --- webui/src/routes/issues/-route.test.tsx | 29 +- .../issues/-ui/issue-detail-modal.module.css | 291 ++++++++++-- .../routes/issues/-ui/issue-detail-modal.tsx | 425 +++++++++++------- 3 files changed, 536 insertions(+), 209 deletions(-) diff --git a/webui/src/routes/issues/-route.test.tsx b/webui/src/routes/issues/-route.test.tsx index 4cb6d42f..c750192e 100644 --- a/webui/src/routes/issues/-route.test.tsx +++ b/webui/src/routes/issues/-route.test.tsx @@ -80,6 +80,10 @@ describe('issues route', () => { artist_name: 'Artist', thumb_url: 'https://example.com/thumb.jpg', spotify_album_id: 'abc123', + track_number: 1, + duration: 245, + format: 'FLAC', + bitrate: 1411, }, created_at: '2026-04-03 10:30:00', reporter_name: 'Ada', @@ -101,14 +105,18 @@ describe('issues route', () => { status: 'open', priority: 'normal', snapshot_data: { - title: 'Album Name', - artist_name: 'Artist', - thumb_url: 'https://example.com/thumb.jpg', - spotify_album_id: 'abc123', + title: 'Album Name', + artist_name: 'Artist', + thumb_url: 'https://example.com/thumb.jpg', + spotify_album_id: 'abc123', + track_number: 1, + duration: 245, + format: 'FLAC', + bitrate: 1411, + }, + created_at: '2026-04-03 10:30:00', + reporter_name: 'Ada', }, - created_at: '2026-04-03 10:30:00', - reporter_name: 'Ada', - }, }); } if (url.includes('/api/spotify/album/abc123')) { @@ -248,4 +256,11 @@ describe('issues route', () => { ).toBe(true); }); }); + + it('does not render track details for album issues', async () => { + renderIssuesRoute(['/issues?issueId=7']); + + await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7')); + expect(screen.queryByText('Track Details')).not.toBeInTheDocument(); + }); }); diff --git a/webui/src/routes/issues/-ui/issue-detail-modal.module.css b/webui/src/routes/issues/-ui/issue-detail-modal.module.css index 3c3e6961..b4210d4d 100644 --- a/webui/src/routes/issues/-ui/issue-detail-modal.module.css +++ b/webui/src/routes/issues/-ui/issue-detail-modal.module.css @@ -415,6 +415,44 @@ max-width: 750px; width: 95vw; max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 16px; +} + +.issueDetailModal .modalHeader { + padding: 20px 24px; + gap: 16px; +} + +.issueDetailModal .modalHeaderTitle { + margin: 0; + font-size: 18px; +} + +.issueDetailModal .modalClose { + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + font-size: 16px; +} + +.issueDetailModal .modalBody { + flex: 1 1 auto; + overflow-y: auto; + max-height: calc(85vh - 120px); + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.issueDetailModal .modalFooter { + padding: 16px 24px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + gap: 8px; } .reportIssueModal { @@ -536,6 +574,22 @@ margin-top: 2px; } +.issueHeroGenres { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + +.issueHeroGenreTag { + font-size: 10px; + padding: 2px 8px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(255, 255, 255, 0.08); +} + .issueDetailInfoBar { display: flex; align-items: center; @@ -555,6 +609,10 @@ flex-wrap: wrap; } +.issueDetailInfoRight { + font-size: 12px; +} + .issueDetailCategory { font-size: 13px; color: rgba(255, 255, 255, 0.7); @@ -585,6 +643,14 @@ font-weight: 500; } +.issueDetailSectionCount { + font-weight: 400; + color: rgba(255, 255, 255, 0.3); + font-size: 10px; + text-transform: none; + margin-left: 4px; +} + .issueDetailTitleText { font-size: 15px; font-weight: 500; @@ -666,30 +732,61 @@ margin-bottom: 14px; } +.issueActionDownload, +.issueActionWishlist, +.modalButtonSecondary, +.modalButtonPrimary, +.modalButtonProgress, +.modalButtonResolve, +.modalButtonDismiss, +.modalButtonReopen, +.modalButtonDelete { + min-height: auto; + padding: 8px 16px; + border: none; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + line-height: 1.2; +} + .issueActionDownload { background: rgba(var(--accent-light-rgb), 0.12); color: rgb(var(--accent-light-rgb)); - border-color: rgba(var(--accent-light-rgb), 0.25); } .issueActionDownload:hover:not(:disabled) { background: rgba(var(--accent-light-rgb), 0.22); - border-color: rgba(var(--accent-light-rgb), 0.4); } .issueActionWishlist { background: rgba(255, 165, 0, 0.1); color: #ffa500; - border-color: rgba(255, 165, 0, 0.2); } .issueActionWishlist:hover:not(:disabled) { background: rgba(255, 165, 0, 0.2); - border-color: rgba(255, 165, 0, 0.35); } .issueDetailResponseTextarea { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #fff; + padding: 10px 12px; + border-radius: 8px; + font-size: 13px; + outline: none; + resize: vertical; min-height: 70px; + font-family: inherit; + transition: border-color 0.2s; + box-sizing: border-box; + line-height: 1.5; +} + +.issueDetailResponseTextarea:focus { + border-color: rgba(var(--accent-light-rgb), 0.5); } .issueDetailAdminResponse { @@ -706,12 +803,14 @@ .issueExternalLinks { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 4px; + margin-top: 8px; } .issueExternalLink { display: inline-flex; align-items: center; + gap: 6px; padding: 6px 10px; border-radius: 999px; background: rgba(255, 255, 255, 0.06); @@ -725,29 +824,97 @@ background: rgba(255, 255, 255, 0.1); } -.issueDetailTracklist { - display: grid; - gap: 6px; +.issueExternalLinkService { + font-weight: 600; } -.issueDetailTracklistRow, -.issueDetailTracklistDisc { +.issueExternalLinkType { + font-size: 10px; + font-weight: 400; + opacity: 0.85; +} + +.issueExternalLinkSpotify { + background: rgba(30, 215, 96, 0.1); + color: #1ed760; +} + +.issueExternalLinkSpotify:hover { + background: rgba(30, 215, 96, 0.2); +} + +.issueExternalLinkMusicBrainz { + background: rgba(186, 81, 163, 0.1); + color: #d4a0cb; +} + +.issueExternalLinkMusicBrainz:hover { + background: rgba(186, 81, 163, 0.2); +} + +.issueExternalLinkDeezer { + background: rgba(160, 54, 255, 0.1); + color: #c88eff; +} + +.issueExternalLinkDeezer:hover { + background: rgba(160, 54, 255, 0.2); +} + +.issueExternalLinkTidal { + background: rgba(0, 255, 255, 0.08); + color: #66ffff; +} + +.issueExternalLinkTidal:hover { + background: rgba(0, 255, 255, 0.15); +} + +.issueExternalLinkQobuz { + background: rgba(0, 130, 200, 0.1); + color: #5bb8e8; +} + +.issueExternalLinkQobuz:hover { + background: rgba(0, 130, 200, 0.2); +} + +.issueDetailTracklist { display: grid; - grid-template-columns: 42px minmax(0, 1fr) auto auto; - gap: 10px; - align-items: center; - padding: 8px 10px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.03); + gap: 0; + background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.05); - font-size: 12px; + border-radius: 10px; + padding: 6px 0; + max-height: 240px; + overflow-y: auto; } .issueDetailTracklistDisc { display: block; - color: rgba(255, 255, 255, 0.45); + font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; + color: rgba(255, 255, 255, 0.35); + padding: 8px 14px 4px; + border-top: 1px solid rgba(255, 255, 255, 0.04); +} + +.issueDetailTracklistDisc:first-child { + border-top: none; + padding-top: 4px; +} + +.issueDetailTracklistRow { + display: flex; + align-items: center; + padding: 4px 14px; + gap: 10px; + transition: background 0.15s; +} + +.issueDetailTracklistRow:hover { + background: rgba(255, 255, 255, 0.03); } .issueDetailTracklistNum, @@ -756,12 +923,36 @@ color: rgba(255, 255, 255, 0.42); } +.issueDetailTracklistNum { + font-size: 12px; + min-width: 22px; + text-align: right; + flex-shrink: 0; +} + .issueDetailTracklistTitle { + font-size: 13px; + color: rgba(255, 255, 255, 0.75); + flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: rgba(255, 255, 255, 0.82); +} + +.issueDetailTracklistDur { + font-size: 11px; + color: rgba(255, 255, 255, 0.3); + flex-shrink: 0; + min-width: 36px; + text-align: right; +} + +.issueDetailTracklistMeta { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; } .issueTrackBadge { @@ -772,7 +963,34 @@ background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.65); font-size: 10px; - margin-left: 4px; + margin-left: 0; +} + +.issueTrackBadgeFlac { + background: rgba(255, 184, 77, 0.15); + color: #ffb84d; +} + +.issueTrackBadgeMp3 { + background: rgba(var(--accent-rgb), 0.15); + color: rgb(var(--accent-light-rgb)); +} + +.issueTrackBadgeOther { + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.5); +} + +.issueTrackBadgeHigh { + color: rgb(var(--accent-light-rgb)); +} + +.issueTrackBadgeMedium { + color: #ffb84d; +} + +.issueTrackBadgeLow { + color: #ff6b6b; } .reportIssueEntityInfo { @@ -802,7 +1020,12 @@ } .modalButtonSecondary { - background: rgba(255, 255, 255, 0.04); + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); +} + +.modalButtonSecondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.15); } .modalButtonPrimary { @@ -819,31 +1042,31 @@ .modalButtonProgress { background: rgba(77, 166, 255, 0.15); color: #4da6ff; - border-color: rgba(77, 166, 255, 0.25); } .modalButtonResolve { background: rgba(74, 222, 128, 0.15); color: #4ade80; - border-color: rgba(74, 222, 128, 0.25); } .modalButtonDismiss { - background: rgba(239, 68, 68, 0.14); - color: #ffd0d0; - border-color: rgba(239, 68, 68, 0.28); + background: rgba(136, 136, 136, 0.15); + color: #888; + border-color: rgba(136, 136, 136, 0.3); +} + +.modalButtonDismiss:hover:not(:disabled) { + background: rgba(136, 136, 136, 0.25); } .modalButtonReopen { background: rgba(var(--accent-light-rgb), 0.15); color: #fff; - border-color: rgba(var(--accent-light-rgb), 0.3); } .modalButtonDelete { background: rgba(239, 68, 68, 0.14); color: #ffd0d0; - border-color: rgba(239, 68, 68, 0.28); } .modalButtonDelete:hover:not(:disabled), @@ -934,6 +1157,18 @@ padding: 0 18px 18px; } + .issueDetailModal { + max-height: calc(100vh - 24px); + } + + .issueDetailModal .modalBody { + padding: 18px; + } + + .issueDetailModal .modalFooter { + padding: 0 18px 18px; + } + .issueDetailMetaGrid { grid-template-columns: repeat(1, minmax(0, 1fr)); } diff --git a/webui/src/routes/issues/-ui/issue-detail-modal.tsx b/webui/src/routes/issues/-ui/issue-detail-modal.tsx index ae8c5258..fb571d4a 100644 --- a/webui/src/routes/issues/-ui/issue-detail-modal.tsx +++ b/webui/src/routes/issues/-ui/issue-detail-modal.tsx @@ -1,7 +1,7 @@ import { useMutation } from '@tanstack/react-query'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; -import { Button, FormField, TextArea } from '@/components/form'; +import { Button } from '@/components/form'; import { launchAlbumDownloadWorkflow, launchAlbumWishlistWorkflow, @@ -13,9 +13,8 @@ import { deleteIssue, formatIssueDate, formatStatusLabel, - getEntityDetails, - getEntityLabel, getIssueArtwork, + getPriorityClassName, ISSUE_CATEGORY_META, parseSnapshot, updateIssue, @@ -249,14 +248,18 @@ export function IssueDetailModal({ } const snapshot = issue ? parseSnapshot(issue.snapshot_data) : {}; - const issueDetails = issue ? getEntityDetails(issue, snapshot) : []; const issueArtwork = getIssueArtwork(snapshot); const issueCategoryLabel = issue - ? ISSUE_CATEGORY_META[issue.category]?.label || issue.category + ? `${ISSUE_CATEGORY_META[issue.category]?.icon || ''} ${ + ISSUE_CATEGORY_META[issue.category]?.label || issue.category + }`.trim() : ''; const externalLinks = getExternalLinks(snapshot); const trackMetaItems = getTrackMetaItems(snapshot); const trackRows = Array.isArray(snapshot.tracks) ? snapshot.tracks : []; + const priorityClassName = issue ? getPriorityClassName(issue.priority) : 'normal'; + const albumMetaParts = issue ? getAlbumMetaParts(issue, snapshot) : []; + const genreTags = Array.isArray(snapshot.genres) ? snapshot.genres.slice(0, 5) : []; const albumWorkflowInput = { spotifyAlbumId: String(snapshot.spotify_album_id || ''), artistName: String(snapshot.artist_name || ''), @@ -325,22 +328,61 @@ export function IssueDetailModal({
{String(snapshot.artist_name)}
) : null}
- {String(snapshot.album_title || snapshot.title || issue.title)} + {String( + issue.entity_type === 'artist' + ? snapshot.name || issue.title + : snapshot.album_title || snapshot.title || issue.title, + )}
{issue.entity_type === 'track' ? (
♪ {issue.title}
) : null} - {issue.entity_type === 'artist' && snapshot.name ? ( -
{String(snapshot.name)}
+ {issue.entity_type !== 'artist' && albumMetaParts.length > 0 ? ( +
{albumMetaParts.join(' - ')}
) : null} - {issueDetails.length > 0 ? ( -
{issueDetails.join(' · ')}
+ {genreTags.length > 0 ? ( +
+ {genreTags.map((genre) => ( + + {String(genre)} + + ))} +
+ ) : null} + {externalLinks.length > 0 ? ( +
+ {externalLinks.map((link) => + link.url ? ( + + {link.service} + {link.type} + + ) : ( + + {link.service} + {link.type} + + ), + )} +
) : null}
+ @@ -350,16 +392,45 @@ export function IssueDetailModal({
- Created {formatIssueDate(issue.created_at)} + Reported {formatIssueDate(issue.created_at)} - {issue.reporter_name ? ( + {issue.resolved_at ? ( + + Resolved {formatIssueDate(issue.resolved_at)} + + ) : null} + {issue.reporter_name && isAdmin ? ( by {issue.reporter_name} ) : null}
+ {issue.entity_type !== 'artist' && isAdmin && ( +
+
Admin Actions
+
+ + +
+
+ )} +
-
Issue Details
+
Issue
{issue.title}
-
-
Context
-
-
- - Entity - - {getEntityLabel(issue.entity_type)} - -
-
- # - Entity ID - {issue.entity_id} -
-
- - Category - - {ISSUE_CATEGORY_META[issue.category]?.label || issue.category} - -
-
- ! - Priority - {issue.priority} -
-
- - Status - {formatStatusLabel(issue.status)} -
-
- - Created - - {formatIssueDate(issue.created_at)} - -
- {issue.updated_at ? ( -
- U - Updated - - {formatIssueDate(issue.updated_at)} - -
- ) : null} - {issue.resolved_at ? ( -
- R - Resolved - - {formatIssueDate(issue.resolved_at)} - -
- ) : null} - {issue.resolved_by ? ( -
- A - Resolver - {issue.resolved_by} -
- ) : null} - {issue.reporter_name ? ( -
- P - Reporter - {issue.reporter_name} -
- ) : null} -
-
- - {externalLinks.length > 0 ? ( -
-
External Links
-
- {externalLinks.map((link) => ( - - {link.label} - - ))} -
-
- ) : null} - - {trackMetaItems.length > 0 ? ( + {issue.entity_type === 'track' && trackMetaItems.length > 0 ? (
Track Details
@@ -488,79 +466,25 @@ export function IssueDetailModal({ {trackRows.length > 0 ? (
- Track Listing ({trackRows.length} tracks) + Track Listing {trackRows.length} tracks
- {trackRows.map((track, index) => { - const format = String(track.format || '').toUpperCase(); - const bitrate = track.bitrate ? `${track.bitrate}k` : ''; - const duration = formatDuration(track.duration); - return ( -
- - {String(track.track_number || index + 1)} - - - {String(track.title || 'Unknown')} - - {duration} - - {format ? ( - {format} - ) : null} - {bitrate ? ( - {bitrate} - ) : null} - -
- ); - })} + {renderTrackListing(trackRows)}
) : null} - {issue.entity_type !== 'artist' && isAdmin && ( -
-
Admin Actions
-
- - -
-
- )} - {isAdmin && (
- -