@morscherlab/mint-sdk 1.4.0-beta.18 → 1.4.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/JobsStatusTray.vue.d.ts +4 -0
- package/dist/composables/useJobCenter.d.ts +32 -2
- package/dist/{data-BIiV4kKw.js → data-DBnmeBZX.js} +2 -2
- package/dist/{data-BIiV4kKw.js.map → data-DBnmeBZX.js.map} +1 -1
- package/dist/data.js +2 -2
- package/dist/index.js +3 -3
- package/dist/{install-Dg-Z6ze5.js → install-C5psB1H0.js} +3 -3
- package/dist/{install-Dg-Z6ze5.js.map → install-C5psB1H0.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{runtime-cHv1phcc.js → runtime-CoHhBP10.js} +23 -4
- package/dist/runtime-CoHhBP10.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +2 -2
- package/dist/styles.css +4 -0
- package/dist/useRealtime-C3whhVvb.js +508 -0
- package/dist/useRealtime-C3whhVvb.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +242 -6
- package/src/__tests__/composables/useJobCenter.test.ts +39 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -0
- package/src/components/JobsStatusTray.story.vue +20 -1
- package/src/components/JobsStatusTray.vue +145 -40
- package/src/composables/useJobCenter.ts +63 -2
- package/src/runtime.ts +2 -0
- package/src/styles/components/jobs-status-tray.css +5 -0
- package/dist/runtime-cHv1phcc.js.map +0 -1
- package/dist/useRealtime-BW02xy4Z.js +0 -398
- package/dist/useRealtime-BW02xy4Z.js.map +0 -1
|
@@ -27,20 +27,26 @@ import {
|
|
|
27
27
|
X,
|
|
28
28
|
} from 'lucide-vue-next'
|
|
29
29
|
|
|
30
|
-
import { isActiveJobStatus, jobStatusLabel
|
|
30
|
+
import { isActiveJobStatus, jobStatusLabel } from '../jobs'
|
|
31
31
|
import type { UseJobCenterReturn } from '../composables/useJobCenter'
|
|
32
32
|
import type { JobState, JobStatus } from '../types/jobs'
|
|
33
|
+
import ConfirmDialog from './ConfirmDialog.vue'
|
|
34
|
+
|
|
35
|
+
type ConfirmAction = 'cancel' | 'delete' | 'clear'
|
|
33
36
|
|
|
34
37
|
interface Props {
|
|
35
38
|
/** Job center from `useJobCenter()`; the parent owns it and opens the panel through it. */
|
|
36
39
|
center: UseJobCenterReturn
|
|
37
40
|
title?: string
|
|
38
41
|
teleportTo?: string | HTMLElement | false
|
|
42
|
+
/** Ask before Cancel, Delete and Clear (`true`), or before the named ones only. Default off. */
|
|
43
|
+
confirm?: boolean | Partial<Record<ConfirmAction, boolean>>
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
const props = withDefaults(defineProps<Props>(), {
|
|
42
47
|
title: 'Jobs',
|
|
43
48
|
teleportTo: 'body',
|
|
49
|
+
confirm: false,
|
|
44
50
|
})
|
|
45
51
|
|
|
46
52
|
const emit = defineEmits<{
|
|
@@ -70,15 +76,19 @@ const panelRef = ref<HTMLElement | null>(null)
|
|
|
70
76
|
const teleportTarget = computed(() => props.teleportTo || 'body')
|
|
71
77
|
const tray = props.center
|
|
72
78
|
const titleId = `${tray.panelId}-title`
|
|
79
|
+
const rows = (jobs: JobState[]) => jobs.map(job => ({ job, view: tray.rowView(job) }))
|
|
73
80
|
// A job waiting for input is the only one that needs a person: pin it first.
|
|
74
|
-
const activeRows = computed(() =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const activeRows = computed(() => {
|
|
82
|
+
const active = rows(tray.activeJobs.value)
|
|
83
|
+
return [
|
|
84
|
+
...active.filter(row => row.view.status === 'awaiting_input'),
|
|
85
|
+
...active.filter(row => row.view.status !== 'awaiting_input'),
|
|
86
|
+
]
|
|
87
|
+
})
|
|
78
88
|
const sections = computed(() => [
|
|
79
|
-
{ key: 'active', title: 'Active',
|
|
80
|
-
{ key: 'finished', title: 'Finished',
|
|
81
|
-
].filter(section => section.
|
|
89
|
+
{ key: 'active', title: 'Active', rows: activeRows.value },
|
|
90
|
+
{ key: 'finished', title: 'Finished', rows: rows(tray.historyJobs.value) },
|
|
91
|
+
].filter(section => section.rows.length > 0))
|
|
82
92
|
const headCount = computed(() => {
|
|
83
93
|
const parts = [
|
|
84
94
|
[tray.activeJobs.value.length, 'active'],
|
|
@@ -94,9 +104,13 @@ const canClear = computed(() => (
|
|
|
94
104
|
|
|
95
105
|
// Escape returns focus to the element that opened the panel (the top-bar chip).
|
|
96
106
|
// The panel is non-modal, so Escape is heard on the document while it is open.
|
|
107
|
+
// An Escape that an inner control handled, or that belongs to a modal dialog
|
|
108
|
+
// above the tray (its own confirm dialog too), leaves the tray open.
|
|
97
109
|
let opener: HTMLElement | null = null
|
|
98
110
|
function onDocumentKeydown(event: KeyboardEvent): void {
|
|
99
|
-
if (event.key
|
|
111
|
+
if (event.key !== 'Escape' || event.defaultPrevented || confirmOpen.value) return
|
|
112
|
+
if (event.target instanceof Element && event.target.closest('[aria-modal="true"]')) return
|
|
113
|
+
closeTray()
|
|
100
114
|
}
|
|
101
115
|
watch(tray.isOpen, async (open) => {
|
|
102
116
|
if (!open) {
|
|
@@ -110,6 +124,18 @@ watch(tray.isOpen, async (open) => {
|
|
|
110
124
|
}, { flush: 'sync' })
|
|
111
125
|
onBeforeUnmount(() => document.removeEventListener('keydown', onDocumentKeydown))
|
|
112
126
|
|
|
127
|
+
// A focused row that leaves the list (an action or a refresh) drops focus to body:
|
|
128
|
+
// note the focused element before the update and keep focus in the panel after it.
|
|
129
|
+
let focusedInPanel: Element | null = null
|
|
130
|
+
watch(tray.jobs, () => {
|
|
131
|
+
const focused = document.activeElement
|
|
132
|
+
focusedInPanel = focused && panelRef.value?.contains(focused) ? focused : null
|
|
133
|
+
}, { flush: 'pre' })
|
|
134
|
+
watch(tray.jobs, () => {
|
|
135
|
+
if (tray.isOpen.value && focusedInPanel && !focusedInPanel.isConnected) panelRef.value?.focus()
|
|
136
|
+
focusedInPanel = null
|
|
137
|
+
}, { flush: 'post' })
|
|
138
|
+
|
|
113
139
|
function closeTray(): void {
|
|
114
140
|
tray.close()
|
|
115
141
|
// Safari leaves a clicked button unfocused (opener = body), and the chip node is
|
|
@@ -121,17 +147,6 @@ function closeTray(): void {
|
|
|
121
147
|
target?.focus()
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
function progressText(job: JobState): string {
|
|
125
|
-
if (job.error) return job.error
|
|
126
|
-
if (job.status === 'awaiting_input') {
|
|
127
|
-
return job.progress.message || 'Waiting for external input'
|
|
128
|
-
}
|
|
129
|
-
if (job.status === 'queued' && job.queue_position > 0) {
|
|
130
|
-
return `Queue position ${job.queue_position}`
|
|
131
|
-
}
|
|
132
|
-
return job.progress.message || job.progress.stage || jobStatusLabel(job.status)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
150
|
function formatTime(value: string | null): string {
|
|
136
151
|
if (!value) return ''
|
|
137
152
|
const date = new Date(value)
|
|
@@ -140,14 +155,89 @@ function formatTime(value: string | null): string {
|
|
|
140
155
|
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
141
156
|
}
|
|
142
157
|
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
/** Right-hand value: the percent while a job moves, else when it ended or started. */
|
|
159
|
+
function jobValue(job: JobState, percent: number | null): string {
|
|
160
|
+
if (percent != null) return `${percent}%`
|
|
161
|
+
return formatTime(job.completed_at || job.started_at || job.created_at)
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
// Confirmation: the action waits for the dialog; it runs on the live job.
|
|
165
|
+
// `ids`: the finished jobs that the user confirmed to clear.
|
|
166
|
+
const pending = ref<{ action: ConfirmAction; job?: JobState; ids?: string[] } | null>(null)
|
|
167
|
+
const confirmOpen = ref(false)
|
|
168
|
+
// Set on request and kept, so the dialog text stays while it fades out.
|
|
169
|
+
const confirmText = ref<{ title: string; message: string; confirmLabel: string; cancelLabel: string } | null>(null)
|
|
170
|
+
const confirmEnabled = computed(() => props.confirm === true
|
|
171
|
+
|| (typeof props.confirm === 'object' && Object.values(props.confirm).some(Boolean)))
|
|
172
|
+
|
|
173
|
+
function textFor(action: ConfirmAction, job?: JobState) {
|
|
174
|
+
if (action === 'clear') {
|
|
175
|
+
const count = tray.deletableJobs.value.length
|
|
176
|
+
return {
|
|
177
|
+
title: 'Clear finished jobs?',
|
|
178
|
+
message: `${count} finished ${count === 1 ? 'job' : 'jobs'} and their stored results are deleted.`,
|
|
179
|
+
confirmLabel: 'Clear jobs',
|
|
180
|
+
cancelLabel: 'Cancel',
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return action === 'cancel'
|
|
184
|
+
? { title: 'Cancel job?', message: `${job?.name} stops. Its partial results are lost.`, confirmLabel: 'Cancel job', cancelLabel: 'Keep running' }
|
|
185
|
+
: { title: 'Delete job?', message: `${job?.name} and its stored result are deleted.`, confirmLabel: 'Delete job', cancelLabel: 'Cancel' }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function needsConfirm(action: ConfirmAction): boolean {
|
|
189
|
+
return props.confirm === true || (typeof props.confirm === 'object' && props.confirm[action] === true)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function request(action: ConfirmAction, job?: JobState): void {
|
|
193
|
+
if (!needsConfirm(action)) {
|
|
194
|
+
void run(action, job)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
pending.value = { action, job, ids: action === 'clear' ? deletableIds() : undefined }
|
|
198
|
+
confirmText.value = textFor(action, job)
|
|
199
|
+
confirmOpen.value = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function deletableIds(): string[] {
|
|
203
|
+
return tray.deletableJobs.value.map(job => job.job_id).sort()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// A new source is a new job list: the confirmed target is gone.
|
|
207
|
+
watch(tray.source, () => onConfirmOpen(false))
|
|
208
|
+
|
|
209
|
+
function onConfirmOpen(open: boolean): void {
|
|
210
|
+
confirmOpen.value = open
|
|
211
|
+
if (!open) pending.value = null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function confirmPending(): Promise<void> {
|
|
215
|
+
// A second Confirm click while the dialog fades out does nothing.
|
|
216
|
+
const current = pending.value
|
|
217
|
+
if (!confirmOpen.value || !current) return
|
|
218
|
+
if (current.action === 'clear') {
|
|
219
|
+
// Polling changed the finished jobs: ask again with the new count.
|
|
220
|
+
const ids = deletableIds()
|
|
221
|
+
if (ids.join() !== current.ids?.join()) {
|
|
222
|
+
pending.value = { ...current, ids }
|
|
223
|
+
confirmText.value = textFor('clear')
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
confirmOpen.value = false
|
|
228
|
+
pending.value = null
|
|
229
|
+
if (current.action === 'clear') return clearFinished()
|
|
230
|
+
// Polling can end or remove the job while the dialog is open: then do nothing.
|
|
231
|
+
const live = tray.jobs.value.find(job => job.job_id === current.job?.job_id)
|
|
232
|
+
const allowed = live && (current.action === 'cancel'
|
|
233
|
+
? isActiveJobStatus(live.status) && live.can_cancel
|
|
234
|
+
: live.can_delete)
|
|
235
|
+
if (allowed) await run(current.action, live)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function run(action: ConfirmAction, job?: JobState): Promise<void> {
|
|
239
|
+
if (action === 'clear') return clearFinished()
|
|
240
|
+
return action === 'cancel' ? cancelJob(job!) : deleteJob(job!)
|
|
151
241
|
}
|
|
152
242
|
|
|
153
243
|
async function cancelJob(job: JobState): Promise<void> {
|
|
@@ -221,36 +311,37 @@ async function clearFinished(): Promise<void> {
|
|
|
221
311
|
<template v-for="section in sections" :key="section.key">
|
|
222
312
|
<div class="mint-jobs-tray__section" :data-section="section.key">
|
|
223
313
|
{{ section.title }}
|
|
224
|
-
<span class="mint-jobs-tray__section-count">· {{ section.
|
|
314
|
+
<span class="mint-jobs-tray__section-count">· {{ section.rows.length }}</span>
|
|
225
315
|
<button
|
|
226
316
|
v-if="section.key === 'finished' && canClear"
|
|
227
317
|
type="button"
|
|
228
318
|
class="mint-jobs-tray__section-action"
|
|
229
|
-
@click="
|
|
319
|
+
@click="request('clear')"
|
|
230
320
|
>
|
|
231
321
|
Clear
|
|
232
322
|
</button>
|
|
233
323
|
</div>
|
|
234
324
|
|
|
235
325
|
<div
|
|
236
|
-
v-for="job in section.
|
|
326
|
+
v-for="{ job, view } in section.rows"
|
|
237
327
|
:key="job.job_id"
|
|
238
328
|
class="mint-jobs-tray__row"
|
|
239
|
-
:class="{ 'mint-jobs-tray__row--awaiting-input':
|
|
329
|
+
:class="{ 'mint-jobs-tray__row--awaiting-input': view.status === 'awaiting_input' }"
|
|
240
330
|
:data-status="job.status"
|
|
241
331
|
>
|
|
332
|
+
<!-- data-status is the real job status; the row view only changes how it looks. -->
|
|
242
333
|
<component
|
|
243
|
-
:is="STATUS_ICONS[
|
|
334
|
+
:is="STATUS_ICONS[view.status]"
|
|
244
335
|
:size="15"
|
|
245
|
-
:class="['mint-jobs-tray__status', `mint-jobs-tray__status--${
|
|
336
|
+
:class="['mint-jobs-tray__status', `mint-jobs-tray__status--${view.status.replace('_', '-')}`]"
|
|
246
337
|
role="img"
|
|
247
|
-
:aria-label="jobStatusLabel(
|
|
338
|
+
:aria-label="jobStatusLabel(view.status)"
|
|
248
339
|
/>
|
|
249
340
|
<button type="button" class="mint-jobs-tray__row-title" @click="emit('select', job)">
|
|
250
341
|
<slot name="job-title" :job="job">{{ job.name }}</slot>
|
|
251
342
|
</button>
|
|
252
343
|
<span class="mint-jobs-tray__tools">
|
|
253
|
-
<span class="mint-jobs-tray__value">{{ jobValue(job) }}</span>
|
|
344
|
+
<span class="mint-jobs-tray__value">{{ jobValue(job, view.percent) }}</span>
|
|
254
345
|
<slot name="job-actions" :job="job" />
|
|
255
346
|
<button
|
|
256
347
|
v-if="isActiveJobStatus(job.status) && job.can_cancel && tray.canCancel.value"
|
|
@@ -259,7 +350,7 @@ async function clearFinished(): Promise<void> {
|
|
|
259
350
|
:aria-label="`Cancel ${job.name}`"
|
|
260
351
|
title="Cancel"
|
|
261
352
|
:disabled="tray.isPending(job)"
|
|
262
|
-
@click="
|
|
353
|
+
@click="request('cancel', job)"
|
|
263
354
|
>
|
|
264
355
|
<Square :size="12" aria-hidden="true" />
|
|
265
356
|
</button>
|
|
@@ -270,33 +361,47 @@ async function clearFinished(): Promise<void> {
|
|
|
270
361
|
:aria-label="`Delete ${job.name}`"
|
|
271
362
|
title="Delete"
|
|
272
363
|
:disabled="tray.isPending(job)"
|
|
273
|
-
@click="
|
|
364
|
+
@click="request('delete', job)"
|
|
274
365
|
>
|
|
275
366
|
<Trash2 :size="14" aria-hidden="true" />
|
|
276
367
|
</button>
|
|
277
368
|
</span>
|
|
278
369
|
<div
|
|
279
|
-
v-if="
|
|
370
|
+
v-if="view.percent != null"
|
|
280
371
|
class="mint-jobs-tray__progress"
|
|
281
372
|
role="progressbar"
|
|
282
373
|
:aria-label="`${job.name} progress`"
|
|
283
374
|
aria-valuemin="0"
|
|
284
375
|
aria-valuemax="100"
|
|
285
|
-
:aria-valuenow="
|
|
376
|
+
:aria-valuenow="view.percent"
|
|
286
377
|
>
|
|
287
|
-
<span :style="{ width: `${
|
|
378
|
+
<span :style="{ width: `${view.percent}%` }" />
|
|
288
379
|
</div>
|
|
289
380
|
<span
|
|
290
381
|
class="mint-jobs-tray__detail"
|
|
291
382
|
:class="{ 'mint-jobs-tray__detail--error': job.status === 'failed' }"
|
|
292
383
|
>
|
|
293
|
-
<slot name="job-detail" :job="job">{{
|
|
384
|
+
<slot name="job-detail" :job="job">{{ view.message }}</slot>
|
|
294
385
|
</span>
|
|
295
386
|
</div>
|
|
296
387
|
</template>
|
|
297
388
|
</div>
|
|
298
389
|
</section>
|
|
299
390
|
</Transition>
|
|
391
|
+
|
|
392
|
+
<ConfirmDialog
|
|
393
|
+
v-if="confirmEnabled"
|
|
394
|
+
:open="confirmOpen"
|
|
395
|
+
tone="error"
|
|
396
|
+
:title="confirmText?.title"
|
|
397
|
+
:confirm-label="confirmText?.confirmLabel"
|
|
398
|
+
:cancel-label="confirmText?.cancelLabel"
|
|
399
|
+
@update:open="onConfirmOpen"
|
|
400
|
+
@confirm="confirmPending"
|
|
401
|
+
>
|
|
402
|
+
<!-- The marker class lifts the dialog above the tray (jobs-status-tray.css). -->
|
|
403
|
+
<p class="mint-confirm__message mint-jobs-tray__confirm">{{ confirmText?.message }}</p>
|
|
404
|
+
</ConfirmDialog>
|
|
300
405
|
</div>
|
|
301
406
|
</Teleport>
|
|
302
407
|
</template>
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
JobListPayload,
|
|
19
19
|
JobState,
|
|
20
20
|
JobStateInput,
|
|
21
|
+
JobStatus,
|
|
21
22
|
TopBarJob,
|
|
22
23
|
} from '../types/jobs'
|
|
23
24
|
import { usePlatform } from './usePlatform'
|
|
@@ -148,11 +149,41 @@ function duplicatePluginJobId(jobId: string): Error {
|
|
|
148
149
|
export interface UseJobCenterOptions {
|
|
149
150
|
source?: MaybeRefOrGetter<PluginJobCenterSource | undefined>
|
|
150
151
|
loadOnMount?: boolean
|
|
151
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Maps the job that the top-bar chip shows. It is called for the chosen own
|
|
154
|
+
* active job AND for the held just-completed job, so check `job.status`.
|
|
155
|
+
* `null` keeps the default mapping.
|
|
156
|
+
*/
|
|
152
157
|
topBarJob?: (job: JobState) => TopBarJob | null
|
|
158
|
+
/**
|
|
159
|
+
* Overrides how a tray row looks: `status` (icon, awaiting-input tint and
|
|
160
|
+
* pinning), `percent` (progress bar and value) and `message` (detail line).
|
|
161
|
+
* A missing or `undefined` field, or a `null` status or message, keeps the
|
|
162
|
+
* job's own; `percent: null` hides the bar and shows the time, and a status
|
|
163
|
+
* alone keeps the job's own percent. The job's section, actions and counts
|
|
164
|
+
* do not change.
|
|
165
|
+
*/
|
|
166
|
+
rowJob?: (job: JobState) => JobRowOverride | null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A `rowJob` result; see `UseJobCenterOptions.rowJob`. */
|
|
170
|
+
export interface JobRowOverride {
|
|
171
|
+
status?: JobStatus | null
|
|
172
|
+
percent?: number | null
|
|
173
|
+
message?: string | null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** How a tray row shows a job; see `UseJobCenterReturn.rowView`. */
|
|
177
|
+
export interface JobRowView {
|
|
178
|
+
status: JobStatus
|
|
179
|
+
/** 0-100, rounded; `null` hides the progress bar and shows the time instead. */
|
|
180
|
+
percent: number | null
|
|
181
|
+
message: string
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
export interface UseJobCenterReturn {
|
|
185
|
+
/** The current source; a new identity is a new job list. */
|
|
186
|
+
source: ComputedRef<PluginJobCenterSource | undefined>
|
|
156
187
|
jobs: ComputedRef<JobState[]>
|
|
157
188
|
activeJobs: ComputedRef<JobState[]>
|
|
158
189
|
historyJobs: ComputedRef<JobState[]>
|
|
@@ -164,6 +195,8 @@ export interface UseJobCenterReturn {
|
|
|
164
195
|
canClearFinished: ComputedRef<boolean>
|
|
165
196
|
/** DOM id of the JobsStatusTray panel, for `AppTopBar`'s `jobsPanelId` prop. */
|
|
166
197
|
panelId: string
|
|
198
|
+
/** The row view of a job, with the `rowJob` option applied. */
|
|
199
|
+
rowView: (job: JobState) => JobRowView
|
|
167
200
|
/** The viewer's own job for `AppTopBar`'s `job` prop. */
|
|
168
201
|
topBarJob: ComputedRef<TopBarJob | null>
|
|
169
202
|
/** Jobs in the list with status `completed`, for `AppTopBar`'s `jobsDone` prop. */
|
|
@@ -202,6 +235,17 @@ function writeSeenFailed(key: string, seen: Set<string>): void {
|
|
|
202
235
|
}
|
|
203
236
|
}
|
|
204
237
|
|
|
238
|
+
function jobMessage(job: JobState): string {
|
|
239
|
+
if (job.error) return job.error
|
|
240
|
+
if (job.status === 'awaiting_input') {
|
|
241
|
+
return job.progress.message || 'Waiting for external input'
|
|
242
|
+
}
|
|
243
|
+
if (job.status === 'queued' && job.queue_position > 0) {
|
|
244
|
+
return `Queue position ${job.queue_position}`
|
|
245
|
+
}
|
|
246
|
+
return job.progress.message || job.progress.stage || jobStatusLabel(job.status)
|
|
247
|
+
}
|
|
248
|
+
|
|
205
249
|
function errorMessage(error: unknown, fallback: string): string {
|
|
206
250
|
return error instanceof Error ? error.message : fallback
|
|
207
251
|
}
|
|
@@ -302,11 +346,26 @@ export function useJobCenter(
|
|
|
302
346
|
percent: shown.status === 'running' ? normalizeJobPercent(shown.progress.percent) : null,
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
|
-
|
|
349
|
+
const held = heldJob.value
|
|
350
|
+
if (held) return options.topBarJob?.(held) ?? { status: 'completed', label: `${held.name} completed` }
|
|
306
351
|
const failed = unseenFailedJobs.value.length
|
|
307
352
|
return failed > 0 ? { status: 'failed', label: `${failed} failed` } : null
|
|
308
353
|
})
|
|
309
354
|
|
|
355
|
+
function rowView(job: JobState): JobRowView {
|
|
356
|
+
const override = options.rowJob?.(job) ?? {}
|
|
357
|
+
const status = override.status ?? job.status
|
|
358
|
+
// Only an explicit percent changes the bar; a status override keeps the job's own.
|
|
359
|
+
const percent = override.percent !== undefined
|
|
360
|
+
? override.percent
|
|
361
|
+
: job.status === 'running' ? job.progress.percent : null
|
|
362
|
+
return {
|
|
363
|
+
status,
|
|
364
|
+
percent: percent == null ? null : Math.round(normalizeJobPercent(percent)),
|
|
365
|
+
message: override.message ?? jobMessage(job),
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
310
369
|
function handleTopBarClick(onSelect: (job: JobState) => void): void {
|
|
311
370
|
const finished = heldJob.value
|
|
312
371
|
if (!isOpen.value && ownActiveJobs.value.length === 0 && finished) onSelect(finished)
|
|
@@ -416,6 +475,7 @@ export function useJobCenter(
|
|
|
416
475
|
})
|
|
417
476
|
|
|
418
477
|
return {
|
|
478
|
+
source,
|
|
419
479
|
jobs,
|
|
420
480
|
activeJobs,
|
|
421
481
|
historyJobs,
|
|
@@ -426,6 +486,7 @@ export function useJobCenter(
|
|
|
426
486
|
canDelete,
|
|
427
487
|
canClearFinished,
|
|
428
488
|
panelId: `mint-jobs-tray-${useId()}`,
|
|
489
|
+
rowView,
|
|
429
490
|
topBarJob,
|
|
430
491
|
completedCount,
|
|
431
492
|
isOpen,
|
package/src/runtime.ts
CHANGED