Merge commit '11942c89ac8cb443bad62198cfcfdc2d5f3647a1' into fleet/story-d2-job-error-banner-home
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
onDelete?: (jobId: string) => void;
|
||||
retryLabel?: string;
|
||||
deleteLabel?: string;
|
||||
detailed?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -17,36 +18,181 @@
|
||||
onDelete,
|
||||
retryLabel = 'Retry',
|
||||
deleteLabel = 'Delete',
|
||||
detailed = false,
|
||||
}: Props = $props();
|
||||
|
||||
const isDetailed = $derived(detailed || Boolean(onRetry || onDelete));
|
||||
|
||||
function handleRetry(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onRetry?.(job.id);
|
||||
}
|
||||
|
||||
function handleDelete(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelete?.(job.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if job.status === 'failed' && job.error}
|
||||
<div class="recent-err-line" role="alert" title={job.error}>
|
||||
<span class="error-text" style="color: {accentColor}">{job.error}</span>
|
||||
{#if onRetry}
|
||||
<button class="retry-btn" onclick={handleRetry} aria-label={retryLabel}>{retryLabel}</button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<button class="delete-btn" onclick={(e) => { e.preventDefault(); e.stopPropagation(); onDelete?.(job.id); }} aria-label={deleteLabel}>{deleteLabel}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isDetailed}
|
||||
<!-- Detailed error banner for detail page -->
|
||||
<div class="error-banner" role="alert">
|
||||
<div class="eb-icon" aria-hidden="true">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 3L2 21h20L12 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="10" x2="12" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="18" r="0.8" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="eb-body">
|
||||
<div class="eb-title">Transcription failed</div>
|
||||
<div class="eb-msg">{job.error}</div>
|
||||
</div>
|
||||
<div class="eb-actions">
|
||||
{#if onRetry}
|
||||
<button class="btn primary" onclick={handleRetry} aria-label="Retry transcription">↺ Retry transcription</button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<button class="btn ghost danger-ghost" onclick={handleDelete}>Delete</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Minimal error line for list view -->
|
||||
<div class="recent-err-line" role="alert" title={job.error}>
|
||||
<span class="error-text" style="color: {accentColor}">{job.error}</span>
|
||||
{#if onRetry}
|
||||
<button class="retry-btn" onclick={handleRetry} aria-label={retryLabel}>{retryLabel}</button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<button class="delete-btn" onclick={handleDelete} aria-label={deleteLabel}>{deleteLabel}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if job.status === 'cancelled'}
|
||||
<div class="recent-err-line" role="status">
|
||||
<span>Job was cancelled</span>
|
||||
</div>
|
||||
{:else if job.status === 'done' && job.segmentsJson}
|
||||
<div class="recent-err-line">
|
||||
<button class="delete-btn" onclick={(e) => { e.preventDefault(); e.stopPropagation(); onDelete?.(job.id); }} aria-label={deleteLabel}>{deleteLabel}</button>
|
||||
{#if onDelete}
|
||||
<button class="delete-btn" onclick={handleDelete} aria-label={deleteLabel}>{deleteLabel}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.error-banner {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
padding: 16px 18px;
|
||||
border-radius: 10px;
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eb-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
background: rgba(255,90,90,0.12);
|
||||
color: var(--error-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eb-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eb-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.eb-msg {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255,180,180,0.85);
|
||||
margin-top: 5px;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 96px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.eb-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12.5px;
|
||||
font-weight: 550;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 13px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 140ms ease, border-color 140ms ease;
|
||||
}
|
||||
.btn:hover {
|
||||
background: rgba(255,255,255,0.07);
|
||||
border-color: rgba(255,255,255,0.18);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px var(--accent-ring);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #10120a;
|
||||
font-weight: 600;
|
||||
padding: 10px 18px;
|
||||
font-size: 13.5px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 18px rgba(205,242,78,0.18);
|
||||
}
|
||||
.btn.primary:hover {
|
||||
background: #d9f566;
|
||||
border-color: #d9f566;
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
.btn.ghost:hover {
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.btn.danger-ghost {
|
||||
color: var(--error-text);
|
||||
}
|
||||
.btn.danger-ghost:hover {
|
||||
background: rgba(255,90,90,0.08);
|
||||
border-color: var(--error-border);
|
||||
}
|
||||
|
||||
.recent-err-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getDisplayJobProgress, getJobStatusLabel, isTerminalJobStatus } from '$lib/job-progress.js';
|
||||
import SourceIcon from '$lib/components/SourceIcon.svelte';
|
||||
import Waveform from '$lib/components/Waveform.svelte';
|
||||
import JobErrorLine from '$lib/components/JobErrorLine.svelte';
|
||||
import { accent } from '$lib/accent.js';
|
||||
|
||||
const ACCENT = $derived($accent.value);
|
||||
@@ -257,24 +258,8 @@
|
||||
{/if}
|
||||
|
||||
<!-- ── Error ─────────────────────────────────────────── -->
|
||||
{#if job.error}
|
||||
<div class="error-banner" role="alert">
|
||||
<div class="eb-icon" aria-hidden="true">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 3L2 21h20L12 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="10" x2="12" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="18" r="0.8" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="eb-body">
|
||||
<div class="eb-title">Transcription failed</div>
|
||||
<div class="eb-msg">{job.error}</div>
|
||||
</div>
|
||||
<div class="eb-actions">
|
||||
<button class="btn primary" onclick={retryJobAction} aria-label="Retry transcription">↺ Retry transcription</button>
|
||||
<button class="btn ghost danger-ghost" onclick={deleteJob}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if job && job.error}
|
||||
<JobErrorLine {job} accentColor={ACCENT} onRetry={retryJobAction} onDelete={deleteJob} />
|
||||
{/if}
|
||||
|
||||
<!-- ── Two-column: pipeline + downloads/transcript ───── -->
|
||||
@@ -406,111 +391,12 @@
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
padding: 16px 18px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
background: var(--error-bg);
|
||||
border: 1px solid var(--error-border);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eb-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
background: rgba(255,90,90,0.12);
|
||||
color: var(--error-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eb-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eb-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.eb-msg {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255,180,180,0.85);
|
||||
margin-top: 5px;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 96px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.eb-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12.5px;
|
||||
font-weight: 550;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 13px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 140ms ease, border-color 140ms ease;
|
||||
}
|
||||
.btn:hover {
|
||||
background: rgba(255,255,255,0.07);
|
||||
border-color: rgba(255,255,255,0.18);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px var(--accent-ring);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #10120a;
|
||||
font-weight: 600;
|
||||
padding: 10px 18px;
|
||||
font-size: 13.5px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 18px rgba(205,242,78,0.18);
|
||||
}
|
||||
.btn.primary:hover {
|
||||
background: #d9f566;
|
||||
border-color: #d9f566;
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
.btn.ghost:hover {
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.btn.danger-ghost {
|
||||
color: var(--error-text);
|
||||
}
|
||||
.btn.danger-ghost:hover {
|
||||
background: rgba(255,90,90,0.08);
|
||||
border-color: var(--error-border);
|
||||
background: rgba(255, 90, 90, 0.08);
|
||||
border: 1px solid rgba(255, 90, 90, 0.2);
|
||||
color: #ff8a8a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Breadcrumb ─────────────────────────────────────────── */
|
||||
|
||||
@@ -35,13 +35,14 @@ describe('JobErrorLine component', () => {
|
||||
expect(getByText('transcription timeout')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows retry button', () => {
|
||||
it('shows retry button in detailed mode', () => {
|
||||
const { getByRole } = render(JobErrorLine, {
|
||||
job: makeJob(),
|
||||
accentColor: '#cdf24e',
|
||||
onRetry: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
expect(getByRole('button', { name: 'Retry' })).toBeTruthy();
|
||||
// Detailed mode shows "Retry transcription"
|
||||
expect(getByRole('button', { name: 'Retry transcription' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onRetry when retry button clicked', async () => {
|
||||
@@ -51,11 +52,11 @@ describe('JobErrorLine component', () => {
|
||||
accentColor: '#cdf24e',
|
||||
onRetry
|
||||
});
|
||||
await fireEvent.click(getByRole('button', { name: 'Retry' }));
|
||||
await fireEvent.click(getByRole('button', { name: 'Retry transcription' }));
|
||||
expect(onRetry).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows delete button when onDelete provided', () => {
|
||||
it('shows delete button when onDelete provided in detailed mode', () => {
|
||||
const { getByRole } = render(JobErrorLine, {
|
||||
job: makeJob(),
|
||||
accentColor: '#cdf24e',
|
||||
@@ -65,16 +66,18 @@ describe('JobErrorLine component', () => {
|
||||
expect(getByRole('button', { name: 'Delete' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides delete button when onDelete not provided', () => {
|
||||
it('hides delete button when onDelete not provided in detailed mode', () => {
|
||||
const { queryByRole } = render(JobErrorLine, {
|
||||
job: makeJob(),
|
||||
accentColor: '#cdf24e',
|
||||
onRetry: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
expect(queryByRole('button', { name: 'Delete' })).toBeNull();
|
||||
// In detailed mode, only shows delete button if onDelete is provided
|
||||
const deleteButton = queryByRole('button', { name: 'Delete' });
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
|
||||
it('container has role alert for failed jobs', () => {
|
||||
it('container has role alert for failed jobs in detailed mode', () => {
|
||||
const { getByRole } = render(JobErrorLine, {
|
||||
job: makeJob({ status: 'failed' }),
|
||||
accentColor: '#cdf24e',
|
||||
@@ -86,27 +89,37 @@ describe('JobErrorLine component', () => {
|
||||
it('container has role status for cancelled jobs', () => {
|
||||
const { getByRole } = render(JobErrorLine, {
|
||||
job: makeJob({ status: 'cancelled' }),
|
||||
accentColor: '#cdf24e',
|
||||
onRetry: vi.fn().mockResolvedValue(undefined)
|
||||
accentColor: '#cdf24e'
|
||||
});
|
||||
expect(getByRole('status')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does not render when job has no error text', () => {
|
||||
it('does not render error when job has no error text', () => {
|
||||
const { container } = render(JobErrorLine, {
|
||||
job: makeJob({ status: 'failed', error: null }),
|
||||
accentColor: '#cdf24e',
|
||||
onRetry: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
// Should not render any error container
|
||||
expect(container.querySelector('.error-banner')).toBeNull();
|
||||
expect(container.querySelector('.recent-err-line')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders "Job was cancelled" for cancelled jobs', () => {
|
||||
const { getByText } = render(JobErrorLine, {
|
||||
job: makeJob({ status: 'cancelled' }),
|
||||
accentColor: '#cdf24e',
|
||||
onRetry: vi.fn().mockResolvedValue(undefined)
|
||||
accentColor: '#cdf24e'
|
||||
});
|
||||
expect(getByText('Job was cancelled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders detailed display with title when handlers provided', () => {
|
||||
const { getByText } = render(JobErrorLine, {
|
||||
job: makeJob(),
|
||||
accentColor: '#cdf24e',
|
||||
onRetry: vi.fn().mockResolvedValue(undefined),
|
||||
onDelete: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
expect(getByText('Transcription failed')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user