Refine issue detail modal rendering
- use a scoped renderer for the loading, error, and success lifecycle - keep Show for the larger conditional blocks inside the success view - simplify small pending-label branches back to plain ternaries
This commit is contained in:
parent
6471b291fa
commit
892334007d
1 changed files with 231 additions and 205 deletions
|
|
@ -3,6 +3,7 @@ import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
|||
|
||||
import { DialogBody, DialogFooter, DialogFrame, DialogHeader } from '@/components/dialog';
|
||||
import { Button } from '@/components/form';
|
||||
import { Show } from '@/components/primitives';
|
||||
import { useProfile } from '@/platform/shell/route-controllers';
|
||||
import {
|
||||
launchAlbumDownloadWorkflow,
|
||||
|
|
@ -210,207 +211,7 @@ export function IssueDetailModal({
|
|||
title={issue ? `Issue #${issue.id}` : 'Issue details'}
|
||||
closeLabel="Close issue detail"
|
||||
/>
|
||||
<DialogBody>
|
||||
{queryLoading ? (
|
||||
<div className={styles.issuesLoading}>
|
||||
<div className={styles.issuesSpinner} />
|
||||
Loading issue details...
|
||||
</div>
|
||||
) : queryError ? (
|
||||
<div className={styles.issuesEmpty}>
|
||||
<div className={styles.issuesEmptyTitle}>Failed to load issue</div>
|
||||
<div className={styles.issuesEmptyText}>
|
||||
{queryError instanceof Error ? queryError.message : 'Unknown error'}
|
||||
</div>
|
||||
</div>
|
||||
) : issue ? (
|
||||
<>
|
||||
<div className={styles.issueHero}>
|
||||
<div className={styles.issueHeroArtGroup}>
|
||||
{issue.entity_type === 'artist' && issueArtwork ? (
|
||||
<img className={styles.issueHeroArtistThumb} src={issueArtwork} alt="" />
|
||||
) : null}
|
||||
{issueArtwork ? (
|
||||
<img className={styles.issueHeroAlbumArt} src={issueArtwork} alt="" />
|
||||
) : (
|
||||
<div className={styles.issueHeroAlbumPlaceholder}>
|
||||
{issueCategoryMeta?.icon || ISSUE_CATEGORY_META.other.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.issueHeroInfo}>
|
||||
{issue.entity_type !== 'artist' && snapshot.artist_name ? (
|
||||
<div className={styles.issueHeroArtist}>{String(snapshot.artist_name)}</div>
|
||||
) : null}
|
||||
<div className={styles.issueHeroAlbum}>
|
||||
{String(
|
||||
issue.entity_type === 'artist'
|
||||
? snapshot.name || issue.title
|
||||
: snapshot.album_title || snapshot.title || issue.title,
|
||||
)}
|
||||
</div>
|
||||
{issue.entity_type === 'track' ? (
|
||||
<div className={styles.issueHeroTrackName}>♪ {issue.title}</div>
|
||||
) : null}
|
||||
{issue.entity_type !== 'artist' && albumMetaParts.length > 0 ? (
|
||||
<div className={styles.issueHeroMeta}>{albumMetaParts.join(' - ')}</div>
|
||||
) : null}
|
||||
{genreTags.length > 0 ? (
|
||||
<div className={styles.issueHeroGenres}>
|
||||
{genreTags.map((genre) => (
|
||||
<span className={styles.issueHeroGenreTag} key={String(genre)}>
|
||||
{String(genre)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{externalLinks.length > 0 ? (
|
||||
<div className={styles.issueExternalLinks}>
|
||||
{externalLinks.map((link) =>
|
||||
link.url ? (
|
||||
<a
|
||||
key={`${link.service}-${link.type}-${link.label}`}
|
||||
className={`${styles.issueExternalLink} ${styles[link.className]}`}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={`${link.service} ${link.type}`}
|
||||
>
|
||||
<span className={styles.issueExternalLinkService}>{link.service}</span>
|
||||
<span className={styles.issueExternalLinkType}>{link.type}</span>
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
key={`${link.service}-${link.type}-${link.label}`}
|
||||
className={`${styles.issueExternalLink} ${styles[link.className]}`}
|
||||
title={`${link.service} ${link.type}: ${link.id}`}
|
||||
>
|
||||
<span className={styles.issueExternalLinkService}>{link.service}</span>
|
||||
<span className={styles.issueExternalLinkType}>{link.type}</span>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.issueDetailInfoBar}>
|
||||
<div className={styles.issueDetailInfoLeft}>
|
||||
<span
|
||||
className={`${styles.issuePriorityDot} ${getPriorityDotClassName(priorityLevel)}`}
|
||||
/>
|
||||
<span className={`${styles.issueStatusBadge} ${getStatusClassName(issue.status)}`}>
|
||||
{formatStatusLabel(issue.status)}
|
||||
</span>
|
||||
<span className={styles.issueDetailCategory}>{issueCategoryLabel}</span>
|
||||
</div>
|
||||
<div className={styles.issueDetailInfoRight}>
|
||||
<span className={styles.issueDetailDate}>
|
||||
Reported {formatIssueDate(issue.created_at)}
|
||||
</span>
|
||||
{issue.resolved_at ? (
|
||||
<span className={styles.issueDetailDate}>
|
||||
Resolved {formatIssueDate(issue.resolved_at)}
|
||||
</span>
|
||||
) : null}
|
||||
{issue.reporter_name && isAdmin ? (
|
||||
<span className={styles.issueDetailProfile}>by {issue.reporter_name}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issue.entity_type !== 'artist' && isAdmin && (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Admin Actions</div>
|
||||
<div className={styles.issueActionButtons}>
|
||||
<Button
|
||||
className={styles.issueActionDownload}
|
||||
type="button"
|
||||
disabled={downloadWorkflowMutation.isPending}
|
||||
onClick={() => downloadWorkflowMutation.mutate(albumWorkflowInput)}
|
||||
>
|
||||
{downloadWorkflowMutation.isPending ? 'Loading...' : 'Download Album'}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.issueActionWishlist}
|
||||
type="button"
|
||||
disabled={wishlistWorkflowMutation.isPending}
|
||||
onClick={() => wishlistWorkflowMutation.mutate(albumWorkflowInput)}
|
||||
>
|
||||
{wishlistWorkflowMutation.isPending ? 'Loading...' : 'Add to Wishlist'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Issue</div>
|
||||
<div className={styles.issueDetailTitleText}>{issue.title}</div>
|
||||
<div
|
||||
className={
|
||||
issue.description ? styles.issueDetailDescription : styles.issueDetailNoDesc
|
||||
}
|
||||
>
|
||||
{issue.description || 'No additional details provided'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issue.entity_type === 'track' && trackMetaItems.length > 0 ? (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Track Details</div>
|
||||
<div className={styles.issueDetailMetaGrid}>
|
||||
{trackMetaItems.map((item) => (
|
||||
<div className={styles.issueMetaItem} key={item.label}>
|
||||
<span className={styles.issueMetaIcon}>{item.icon}</span>
|
||||
<span className={styles.issueMetaLabel}>{item.label}</span>
|
||||
<span className={styles.issueMetaValue}>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{snapshot.file_path ? (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>File Path</div>
|
||||
<div className={styles.issueDetailFilepath}>{String(snapshot.file_path)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{trackRows.length > 0 ? (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>
|
||||
Track Listing{' '}
|
||||
<span className={styles.issueDetailSectionCount}>{trackRows.length} tracks</span>
|
||||
</div>
|
||||
<div className={styles.issueDetailTracklist}>{renderTrackListing(trackRows)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isAdmin && (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Admin Response</div>
|
||||
<textarea
|
||||
className={styles.issueDetailResponseTextarea}
|
||||
id="issue-detail-response-input"
|
||||
value={adminResponse}
|
||||
onChange={(event) => setAdminResponse(event.target.value)}
|
||||
placeholder="Write a response to the reporter..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAdmin && issue.admin_response ? (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Admin Response</div>
|
||||
<div className={styles.issueDetailAdminResponse}>{issue.admin_response}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</DialogBody>
|
||||
<DialogBody>{renderIssueDetailContent()}</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button className={styles.modalButtonSecondary} type="button" onClick={onClose}>
|
||||
Close
|
||||
|
|
@ -437,6 +238,231 @@ export function IssueDetailModal({
|
|||
</DialogFooter>
|
||||
</DialogFrame>
|
||||
);
|
||||
|
||||
function renderIssueDetailContent() {
|
||||
if (queryLoading) {
|
||||
return (
|
||||
<div className={styles.issuesLoading}>
|
||||
<div className={styles.issuesSpinner} />
|
||||
Loading issue details...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (queryError) {
|
||||
return (
|
||||
<div className={styles.issuesEmpty}>
|
||||
<div className={styles.issuesEmptyTitle}>Failed to load issue</div>
|
||||
<div className={styles.issuesEmptyText}>
|
||||
{queryError instanceof Error ? queryError.message : 'Unknown error'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!issue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.issueHero}>
|
||||
<div className={styles.issueHeroArtGroup}>
|
||||
<Show when={issue.entity_type === 'artist' && issueArtwork}>
|
||||
<img className={styles.issueHeroArtistThumb} src={issueArtwork} alt="" />
|
||||
</Show>
|
||||
<Show
|
||||
when={issueArtwork}
|
||||
fallback={
|
||||
<div className={styles.issueHeroAlbumPlaceholder}>
|
||||
{issueCategoryMeta?.icon || ISSUE_CATEGORY_META.other.icon}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img className={styles.issueHeroAlbumArt} src={issueArtwork} alt="" />
|
||||
</Show>
|
||||
</div>
|
||||
<div className={styles.issueHeroInfo}>
|
||||
<Show when={issue.entity_type !== 'artist' && snapshot.artist_name}>
|
||||
{(artistName) => <div className={styles.issueHeroArtist}>{String(artistName)}</div>}
|
||||
</Show>
|
||||
<div className={styles.issueHeroAlbum}>
|
||||
{String(
|
||||
issue.entity_type === 'artist'
|
||||
? snapshot.name || issue.title
|
||||
: snapshot.album_title || snapshot.title || issue.title,
|
||||
)}
|
||||
</div>
|
||||
<Show when={issue.entity_type === 'track'}>
|
||||
<div className={styles.issueHeroTrackName}>♪ {issue.title}</div>
|
||||
</Show>
|
||||
<Show when={issue.entity_type !== 'artist' && albumMetaParts.length > 0}>
|
||||
<div className={styles.issueHeroMeta}>{albumMetaParts.join(' - ')}</div>
|
||||
</Show>
|
||||
<Show when={genreTags.length}>
|
||||
<div className={styles.issueHeroGenres}>
|
||||
{genreTags.map((genre) => (
|
||||
<span className={styles.issueHeroGenreTag} key={String(genre)}>
|
||||
{String(genre)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={externalLinks.length}>
|
||||
<div className={styles.issueExternalLinks}>
|
||||
{externalLinks.map((link) => (
|
||||
<Show
|
||||
key={`${link.service}-${link.type}-${link.label}`}
|
||||
when={link.url}
|
||||
fallback={
|
||||
<span
|
||||
className={`${styles.issueExternalLink} ${styles[link.className]}`}
|
||||
title={`${link.service} ${link.type}: ${link.id}`}
|
||||
>
|
||||
<span className={styles.issueExternalLinkService}>{link.service}</span>
|
||||
<span className={styles.issueExternalLinkType}>{link.type}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<a
|
||||
className={`${styles.issueExternalLink} ${styles[link.className]}`}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={`${link.service} ${link.type}`}
|
||||
>
|
||||
<span className={styles.issueExternalLinkService}>{link.service}</span>
|
||||
<span className={styles.issueExternalLinkType}>{link.type}</span>
|
||||
</a>
|
||||
</Show>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.issueDetailInfoBar}>
|
||||
<div className={styles.issueDetailInfoLeft}>
|
||||
<span
|
||||
className={`${styles.issuePriorityDot} ${getPriorityDotClassName(priorityLevel)}`}
|
||||
/>
|
||||
<span className={`${styles.issueStatusBadge} ${getStatusClassName(issue.status)}`}>
|
||||
{formatStatusLabel(issue.status)}
|
||||
</span>
|
||||
<span className={styles.issueDetailCategory}>{issueCategoryLabel}</span>
|
||||
</div>
|
||||
<div className={styles.issueDetailInfoRight}>
|
||||
<span className={styles.issueDetailDate}>
|
||||
Reported {formatIssueDate(issue.created_at)}
|
||||
</span>
|
||||
<Show when={issue.resolved_at}>
|
||||
{(resolvedAt) => (
|
||||
<span className={styles.issueDetailDate}>
|
||||
Resolved {formatIssueDate(resolvedAt)}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={isAdmin && issue.reporter_name}>
|
||||
{(reporterName) => (
|
||||
<span className={styles.issueDetailProfile}>by {reporterName}</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={issue.entity_type !== 'artist' && isAdmin}>
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Admin Actions</div>
|
||||
<div className={styles.issueActionButtons}>
|
||||
<Button
|
||||
className={styles.issueActionDownload}
|
||||
type="button"
|
||||
disabled={downloadWorkflowMutation.isPending}
|
||||
onClick={() => downloadWorkflowMutation.mutate(albumWorkflowInput)}
|
||||
>
|
||||
{downloadWorkflowMutation.isPending ? 'Loading...' : 'Download Album'}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.issueActionWishlist}
|
||||
type="button"
|
||||
disabled={wishlistWorkflowMutation.isPending}
|
||||
onClick={() => wishlistWorkflowMutation.mutate(albumWorkflowInput)}
|
||||
>
|
||||
{wishlistWorkflowMutation.isPending ? 'Loading...' : 'Add to Wishlist'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Issue</div>
|
||||
<div className={styles.issueDetailTitleText}>{issue.title}</div>
|
||||
<div
|
||||
className={issue.description ? styles.issueDetailDescription : styles.issueDetailNoDesc}
|
||||
>
|
||||
{issue.description || 'No additional details provided'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={issue.entity_type === 'track' && trackMetaItems.length > 0}>
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Track Details</div>
|
||||
<div className={styles.issueDetailMetaGrid}>
|
||||
{trackMetaItems.map((item) => (
|
||||
<div className={styles.issueMetaItem} key={item.label}>
|
||||
<span className={styles.issueMetaIcon}>{item.icon}</span>
|
||||
<span className={styles.issueMetaLabel}>{item.label}</span>
|
||||
<span className={styles.issueMetaValue}>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={snapshot.file_path}>
|
||||
{(filePath) => (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>File Path</div>
|
||||
<div className={styles.issueDetailFilepath}>{String(filePath)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={trackRows.length}>
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>
|
||||
Track Listing{' '}
|
||||
<span className={styles.issueDetailSectionCount}>{trackRows.length} tracks</span>
|
||||
</div>
|
||||
<div className={styles.issueDetailTracklist}>{renderTrackListing(trackRows)}</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isAdmin}>
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Admin Response</div>
|
||||
<textarea
|
||||
className={styles.issueDetailResponseTextarea}
|
||||
id="issue-detail-response-input"
|
||||
value={adminResponse}
|
||||
onChange={(event) => setAdminResponse(event.target.value)}
|
||||
placeholder="Write a response to the reporter..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isAdmin && issue.admin_response}>
|
||||
{(response) => (
|
||||
<div className={styles.issueDetailSection}>
|
||||
<div className={styles.issueDetailSectionTitle}>Admin Response</div>
|
||||
<div className={styles.issueDetailAdminResponse}>{response}</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTrackListing(trackRows: IssueTrackRow[]) {
|
||||
|
|
@ -470,12 +496,12 @@ function renderTrackListing(trackRows: IssueTrackRow[]) {
|
|||
<span className={styles.issueDetailTracklistTitle}>{String(track.title || 'Unknown')}</span>
|
||||
<span className={styles.issueDetailTracklistDur}>{duration}</span>
|
||||
<span className={styles.issueDetailTracklistMeta}>
|
||||
{format ? (
|
||||
<Show when={format}>
|
||||
<span className={`${styles.issueTrackBadge} ${formatClassName}`}>{format}</span>
|
||||
) : null}
|
||||
{bitrate ? (
|
||||
</Show>
|
||||
<Show when={bitrate}>
|
||||
<span className={`${styles.issueTrackBadge} ${bitrateClassName}`}>{bitrate}</span>
|
||||
) : null}
|
||||
</Show>
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue