@svadmin/ui 0.34.1 → 0.34.3
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/TaskDetails.svelte +33 -13
- package/dist/components/TaskList.svelte +19 -9
- package/dist/components/TaskQueueDrawer.svelte +17 -8
- package/dist/components/TaskQueueDrawer.test.svelte.js +17 -17
- package/dist/components/TaskStatusBadge.svelte +12 -8
- package/dist/components/task-utils.d.ts +14 -0
- package/dist/components/task-utils.js +69 -0
- package/dist/components/task-utils.test.d.ts +1 -0
- package/dist/components/task-utils.test.js +33 -0
- package/package.json +2 -2
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
import TaskProgressBar from './TaskProgressBar.svelte';
|
|
10
10
|
import RetryTaskButton from './RetryTaskButton.svelte';
|
|
11
11
|
import CancelTaskButton from './CancelTaskButton.svelte';
|
|
12
|
+
import {
|
|
13
|
+
canCancelTask,
|
|
14
|
+
canRetryTask,
|
|
15
|
+
resolveTaskCreatedAt,
|
|
16
|
+
resolveTaskError,
|
|
17
|
+
resolveTaskMessage,
|
|
18
|
+
resolveTaskProgress,
|
|
19
|
+
resolveTaskResult,
|
|
20
|
+
resolveTaskTitle,
|
|
21
|
+
resolveTaskUpdatedAt,
|
|
22
|
+
} from './task-utils.js';
|
|
12
23
|
|
|
13
24
|
let {
|
|
14
25
|
task,
|
|
@@ -50,7 +61,9 @@
|
|
|
50
61
|
|
|
51
62
|
function formatDate(value: unknown) {
|
|
52
63
|
if (!value) return '—';
|
|
53
|
-
|
|
64
|
+
const date = new Date(value as string | Date);
|
|
65
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
66
|
+
return date.toLocaleString();
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
function toPrettyJson(value: unknown) {
|
|
@@ -67,7 +80,7 @@
|
|
|
67
80
|
<Card.Title class="text-base">{resolvedTitle}</Card.Title>
|
|
68
81
|
<Card.Description>
|
|
69
82
|
{#if resolvedTask}
|
|
70
|
-
{
|
|
83
|
+
{resolveTaskTitle(resolvedTask)}
|
|
71
84
|
{:else}
|
|
72
85
|
{t('task.detailsSummary')}
|
|
73
86
|
{/if}
|
|
@@ -85,46 +98,53 @@
|
|
|
85
98
|
<Badge variant="outline" class="font-mono text-xs">{resolvedTask.id}</Badge>
|
|
86
99
|
</div>
|
|
87
100
|
|
|
88
|
-
{#if typeof resolvedTask
|
|
89
|
-
<TaskProgressBar value={
|
|
101
|
+
{#if typeof resolveTaskProgress(resolvedTask) === 'number'}
|
|
102
|
+
<TaskProgressBar value={resolveTaskProgress(resolvedTask)} />
|
|
90
103
|
{/if}
|
|
91
104
|
|
|
92
105
|
<div class="grid gap-4 md:grid-cols-2">
|
|
93
106
|
<div class="space-y-1">
|
|
94
107
|
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.createdLabel')}</div>
|
|
95
|
-
<div class="text-sm">{formatDate(resolvedTask
|
|
108
|
+
<div class="text-sm">{formatDate(resolveTaskCreatedAt(resolvedTask))}</div>
|
|
96
109
|
</div>
|
|
97
110
|
<div class="space-y-1">
|
|
98
111
|
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.updatedLabel')}</div>
|
|
99
|
-
<div class="text-sm">{formatDate(resolvedTask
|
|
112
|
+
<div class="text-sm">{formatDate(resolveTaskUpdatedAt(resolvedTask))}</div>
|
|
100
113
|
</div>
|
|
101
114
|
</div>
|
|
102
115
|
|
|
103
|
-
{#if resolvedTask
|
|
116
|
+
{#if resolveTaskMessage(resolvedTask)}
|
|
104
117
|
<div class="space-y-1">
|
|
105
118
|
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.messageLabel')}</div>
|
|
106
|
-
<div class="rounded-md border border-border/60 bg-muted/30 p-3 text-sm">{
|
|
119
|
+
<div class="rounded-md border border-border/60 bg-muted/30 p-3 text-sm">{resolveTaskMessage(resolvedTask)}</div>
|
|
107
120
|
</div>
|
|
108
121
|
{/if}
|
|
109
122
|
|
|
110
|
-
{#if resolvedTask
|
|
123
|
+
{#if resolveTaskError(resolvedTask)}
|
|
111
124
|
<div class="space-y-1">
|
|
112
125
|
<div class="text-xs uppercase tracking-wide text-destructive">{t('task.errorLabel')}</div>
|
|
113
|
-
<pre class="overflow-auto rounded-md border border-destructive/20 bg-destructive/5 p-3 text-xs text-destructive">{toPrettyJson(resolvedTask
|
|
126
|
+
<pre class="overflow-auto rounded-md border border-destructive/20 bg-destructive/5 p-3 text-xs text-destructive">{toPrettyJson(resolveTaskError(resolvedTask))}</pre>
|
|
127
|
+
</div>
|
|
128
|
+
{/if}
|
|
129
|
+
|
|
130
|
+
{#if resolveTaskResult(resolvedTask) !== undefined}
|
|
131
|
+
<div class="space-y-1">
|
|
132
|
+
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.resultLabel')}</div>
|
|
133
|
+
<pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolveTaskResult(resolvedTask))}</pre>
|
|
114
134
|
</div>
|
|
115
135
|
{/if}
|
|
116
136
|
|
|
117
137
|
<div class="space-y-1">
|
|
118
138
|
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.payloadLabel')}</div>
|
|
119
|
-
<pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolvedTask)}</pre>
|
|
139
|
+
<pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolvedTask.payload ?? resolvedTask)}</pre>
|
|
120
140
|
</div>
|
|
121
141
|
|
|
122
142
|
{#if taskProvider?.retry || taskProvider?.cancel}
|
|
123
143
|
<div class="flex flex-wrap gap-2 pt-2">
|
|
124
|
-
{#if taskProvider?.retry &&
|
|
144
|
+
{#if taskProvider?.retry && canRetryTask(resolvedTask)}
|
|
125
145
|
<RetryTaskButton taskId={resolvedTask.id} {taskProvider} />
|
|
126
146
|
{/if}
|
|
127
|
-
{#if taskProvider?.cancel &&
|
|
147
|
+
{#if taskProvider?.cancel && canCancelTask(resolvedTask)}
|
|
128
148
|
<CancelTaskButton taskId={resolvedTask.id} {taskProvider} />
|
|
129
149
|
{/if}
|
|
130
150
|
</div>
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
import TaskProgressBar from './TaskProgressBar.svelte';
|
|
11
11
|
import RetryTaskButton from './RetryTaskButton.svelte';
|
|
12
12
|
import CancelTaskButton from './CancelTaskButton.svelte';
|
|
13
|
+
import {
|
|
14
|
+
canCancelTask,
|
|
15
|
+
canRetryTask,
|
|
16
|
+
resolveTaskMessage,
|
|
17
|
+
resolveTaskProgress,
|
|
18
|
+
resolveTaskTitle,
|
|
19
|
+
resolveTaskUpdatedAt,
|
|
20
|
+
} from './task-utils.js';
|
|
13
21
|
|
|
14
22
|
let {
|
|
15
23
|
tasks,
|
|
@@ -65,7 +73,9 @@
|
|
|
65
73
|
|
|
66
74
|
function formatDate(value: unknown) {
|
|
67
75
|
if (!value) return '—';
|
|
68
|
-
|
|
76
|
+
const date = new Date(value as string | Date);
|
|
77
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
78
|
+
return date.toLocaleString();
|
|
69
79
|
}
|
|
70
80
|
</script>
|
|
71
81
|
|
|
@@ -104,12 +114,12 @@
|
|
|
104
114
|
<Table.Row class="cursor-pointer hover:bg-muted/40" onclick={() => onSelect?.(task)}>
|
|
105
115
|
<Table.Cell class="min-w-[240px]">
|
|
106
116
|
<div class="space-y-1">
|
|
107
|
-
<div class="font-medium text-foreground">{
|
|
108
|
-
{#if task
|
|
109
|
-
<div class="text-xs text-muted-foreground">{
|
|
117
|
+
<div class="font-medium text-foreground">{resolveTaskTitle(task)}</div>
|
|
118
|
+
{#if resolveTaskMessage(task)}
|
|
119
|
+
<div class="text-xs text-muted-foreground">{resolveTaskMessage(task)}</div>
|
|
110
120
|
{/if}
|
|
111
|
-
{#if showProgress && typeof
|
|
112
|
-
<TaskProgressBar value={
|
|
121
|
+
{#if showProgress && typeof resolveTaskProgress(task) === 'number'}
|
|
122
|
+
<TaskProgressBar value={resolveTaskProgress(task)} />
|
|
113
123
|
{/if}
|
|
114
124
|
</div>
|
|
115
125
|
</Table.Cell>
|
|
@@ -117,15 +127,15 @@
|
|
|
117
127
|
<TaskStatusBadge status={String(task.status ?? 'pending')} />
|
|
118
128
|
</Table.Cell>
|
|
119
129
|
<Table.Cell class="text-xs text-muted-foreground">
|
|
120
|
-
{formatDate(task
|
|
130
|
+
{formatDate(resolveTaskUpdatedAt(task))}
|
|
121
131
|
</Table.Cell>
|
|
122
132
|
{#if showActions}
|
|
123
133
|
<Table.Cell class="text-right">
|
|
124
134
|
<div class="flex justify-end gap-2">
|
|
125
|
-
{#if taskProvider?.retry &&
|
|
135
|
+
{#if taskProvider?.retry && canRetryTask(task)}
|
|
126
136
|
<RetryTaskButton taskId={task.id} {taskProvider} />
|
|
127
137
|
{/if}
|
|
128
|
-
{#if taskProvider?.cancel &&
|
|
138
|
+
{#if taskProvider?.cancel && canCancelTask(task)}
|
|
129
139
|
<CancelTaskButton taskId={task.id} {taskProvider} />
|
|
130
140
|
{/if}
|
|
131
141
|
<Button variant="ghost" size="sm" onclick={(e) => { e.stopPropagation(); onSelect?.(task); }}>
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
import * as Card from './ui/card/index.js';
|
|
15
15
|
import TaskList from './TaskList.svelte';
|
|
16
16
|
import TaskDetails from './TaskDetails.svelte';
|
|
17
|
+
import {
|
|
18
|
+
isTaskActive,
|
|
19
|
+
resolveTaskCreatedAt,
|
|
20
|
+
resolveTaskMessage,
|
|
21
|
+
resolveTaskTitle,
|
|
22
|
+
} from './task-utils.js';
|
|
17
23
|
|
|
18
24
|
type AutoRefreshValue = '0' | '5000' | '15000' | '30000';
|
|
19
25
|
type TaskTab = 'tasks' | 'dlq';
|
|
@@ -41,7 +47,9 @@
|
|
|
41
47
|
let submitError = $state<string | null>(null);
|
|
42
48
|
let submitOpen = $state(false);
|
|
43
49
|
|
|
44
|
-
const refreshInterval = $derived
|
|
50
|
+
const refreshInterval = $derived.by<number | false>(() =>
|
|
51
|
+
autoRefresh === '0' ? false : Number(autoRefresh),
|
|
52
|
+
);
|
|
45
53
|
const resolvedTitle = $derived(title ?? t('task.drawerTitle'));
|
|
46
54
|
|
|
47
55
|
const taskQuery = useTaskList({
|
|
@@ -88,10 +96,9 @@
|
|
|
88
96
|
if (!query) return true;
|
|
89
97
|
const haystack = [
|
|
90
98
|
task.id,
|
|
91
|
-
task
|
|
92
|
-
task.name,
|
|
99
|
+
resolveTaskTitle(task),
|
|
93
100
|
task.status,
|
|
94
|
-
task
|
|
101
|
+
resolveTaskMessage(task),
|
|
95
102
|
]
|
|
96
103
|
.filter(Boolean)
|
|
97
104
|
.join(' ')
|
|
@@ -110,7 +117,7 @@
|
|
|
110
117
|
});
|
|
111
118
|
|
|
112
119
|
const runningCount = $derived(
|
|
113
|
-
allTasks.filter(
|
|
120
|
+
allTasks.filter(isTaskActive).length,
|
|
114
121
|
);
|
|
115
122
|
|
|
116
123
|
$effect(() => {
|
|
@@ -142,7 +149,9 @@
|
|
|
142
149
|
|
|
143
150
|
function formatTime(value: unknown) {
|
|
144
151
|
if (!value) return '—';
|
|
145
|
-
|
|
152
|
+
const date = new Date(value as string | Date);
|
|
153
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
154
|
+
return date.toLocaleString();
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
function resetSubmitForm() {
|
|
@@ -223,8 +232,8 @@
|
|
|
223
232
|
<ListTodo class="h-5 w-5 text-muted-foreground" />
|
|
224
233
|
<h2 class="text-sm font-semibold">{resolvedTitle}</h2>
|
|
225
234
|
<Badge variant="secondary" class="text-xs">{t('task.activeCount', { count: runningCount })}</Badge>
|
|
226
|
-
{#if allTasks[0]
|
|
227
|
-
<span class="text-xs text-muted-foreground">{t('task.lastQueuedAt', { time: formatTime(allTasks[0]
|
|
235
|
+
{#if allTasks[0] && resolveTaskCreatedAt(allTasks[0])}
|
|
236
|
+
<span class="text-xs text-muted-foreground">{t('task.lastQueuedAt', { time: formatTime(resolveTaskCreatedAt(allTasks[0])) })}</span>
|
|
228
237
|
{/if}
|
|
229
238
|
</div>
|
|
230
239
|
</div>
|
|
@@ -4,31 +4,31 @@ import TaskQueueDrawer from './TaskQueueDrawer.svelte';
|
|
|
4
4
|
const taskFixtures = [
|
|
5
5
|
{
|
|
6
6
|
id: 'task-1',
|
|
7
|
-
|
|
8
|
-
status: '
|
|
7
|
+
name: 'Generate poster',
|
|
8
|
+
status: 'processing',
|
|
9
9
|
message: 'Rendering hero artwork',
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
created_at: '2026-04-19T10:00:00.000Z',
|
|
11
|
+
updated_at: '2026-04-19T10:05:00.000Z',
|
|
12
12
|
progress: 42,
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
id: 'task-2',
|
|
16
|
-
|
|
16
|
+
name: 'Send email batch',
|
|
17
17
|
status: 'queued',
|
|
18
18
|
message: 'Waiting for worker',
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
created_at: '2026-04-19T10:10:00.000Z',
|
|
20
|
+
updated_at: '2026-04-19T10:12:00.000Z',
|
|
21
21
|
},
|
|
22
22
|
];
|
|
23
23
|
const dlqFixtures = [
|
|
24
24
|
{
|
|
25
25
|
id: 'task-dlq-1',
|
|
26
|
-
|
|
27
|
-
status: '
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
name: 'Nightly sync',
|
|
27
|
+
status: 'dead_lettered',
|
|
28
|
+
error_message: 'Remote API timeout',
|
|
29
|
+
created_at: '2026-04-18T23:00:00.000Z',
|
|
30
|
+
updated_at: '2026-04-18T23:05:00.000Z',
|
|
31
|
+
result_data: { reason: 'Remote API timeout' },
|
|
32
32
|
},
|
|
33
33
|
];
|
|
34
34
|
const mockTaskProvider = {
|
|
@@ -36,10 +36,10 @@ const mockTaskProvider = {
|
|
|
36
36
|
id: 'submitted-image.generate',
|
|
37
37
|
wait: async () => ({
|
|
38
38
|
id: 'submitted-image.generate',
|
|
39
|
-
|
|
39
|
+
name: 'image.generate',
|
|
40
40
|
status: 'queued',
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
created_at: '2026-04-19T11:00:00.000Z',
|
|
42
|
+
updated_at: '2026-04-19T11:00:00.000Z',
|
|
43
43
|
}),
|
|
44
44
|
})),
|
|
45
45
|
get: vi.fn(async (taskId) => {
|
|
@@ -56,7 +56,7 @@ const mutateAsync = vi.fn(async (params) => ({
|
|
|
56
56
|
id: `submitted-${params.taskName}`,
|
|
57
57
|
wait: async () => ({
|
|
58
58
|
id: `submitted-${params.taskName}`,
|
|
59
|
-
|
|
59
|
+
name: params.taskName,
|
|
60
60
|
status: 'queued',
|
|
61
61
|
}),
|
|
62
62
|
}));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
|
-
import { Clock3, Loader2, CheckCircle2, AlertTriangle, CircleDashed } from '@lucide/svelte';
|
|
3
|
+
import { Ban, Clock3, Loader2, CheckCircle2, AlertTriangle, CircleDashed } from '@lucide/svelte';
|
|
4
4
|
import { Badge } from './ui/badge/index.js';
|
|
5
|
+
import { normalizeTaskStatus } from './task-utils.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
status = 'pending',
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
}>();
|
|
15
16
|
|
|
16
17
|
const normalizedStatus = $derived(String(status || 'pending').toLowerCase());
|
|
18
|
+
const lifecycle = $derived(normalizeTaskStatus(normalizedStatus));
|
|
17
19
|
|
|
18
20
|
function translateStatus(value: string) {
|
|
19
21
|
const key = `task.status.${value}`;
|
|
@@ -23,8 +25,7 @@
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const config = $derived.by(() => {
|
|
26
|
-
switch (
|
|
27
|
-
case 'running':
|
|
28
|
+
switch (lifecycle) {
|
|
28
29
|
case 'processing':
|
|
29
30
|
return {
|
|
30
31
|
label: translateStatus(normalizedStatus),
|
|
@@ -33,8 +34,6 @@
|
|
|
33
34
|
iconClass: 'animate-spin',
|
|
34
35
|
};
|
|
35
36
|
case 'completed':
|
|
36
|
-
case 'success':
|
|
37
|
-
case 'done':
|
|
38
37
|
return {
|
|
39
38
|
label: translateStatus(normalizedStatus),
|
|
40
39
|
variant: 'secondary' as const,
|
|
@@ -42,21 +41,26 @@
|
|
|
42
41
|
iconClass: 'text-green-600 dark:text-green-400',
|
|
43
42
|
};
|
|
44
43
|
case 'failed':
|
|
45
|
-
case 'error':
|
|
46
44
|
return {
|
|
47
45
|
label: translateStatus(normalizedStatus),
|
|
48
46
|
variant: 'destructive' as const,
|
|
49
47
|
icon: AlertTriangle,
|
|
50
48
|
iconClass: '',
|
|
51
49
|
};
|
|
50
|
+
case 'cancelled':
|
|
51
|
+
return {
|
|
52
|
+
label: translateStatus(normalizedStatus),
|
|
53
|
+
variant: 'outline' as const,
|
|
54
|
+
icon: Ban,
|
|
55
|
+
iconClass: 'text-muted-foreground',
|
|
56
|
+
};
|
|
52
57
|
case 'queued':
|
|
53
58
|
return {
|
|
54
|
-
label:
|
|
59
|
+
label: translateStatus(normalizedStatus),
|
|
55
60
|
variant: 'outline' as const,
|
|
56
61
|
icon: CircleDashed,
|
|
57
62
|
iconClass: '',
|
|
58
63
|
};
|
|
59
|
-
case 'pending':
|
|
60
64
|
default:
|
|
61
65
|
return {
|
|
62
66
|
label: translateStatus(normalizedStatus),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TaskRecord } from '@svadmin/core';
|
|
2
|
+
export type TaskLifecycle = 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'unknown';
|
|
3
|
+
export declare function normalizeTaskStatus(status: unknown): TaskLifecycle;
|
|
4
|
+
export declare function isTaskActive(task: TaskRecord): boolean;
|
|
5
|
+
export declare function canCancelTask(task: TaskRecord): boolean;
|
|
6
|
+
export declare function canRetryTask(task: TaskRecord): boolean;
|
|
7
|
+
export declare function resolveTaskTitle(task: TaskRecord): string;
|
|
8
|
+
export declare function resolveTaskMessage(task: TaskRecord): string;
|
|
9
|
+
export declare function resolveTaskUpdatedAt(task: TaskRecord): unknown;
|
|
10
|
+
export declare function resolveTaskCreatedAt(task: TaskRecord): unknown;
|
|
11
|
+
export declare function resolveTaskQueueName(task: TaskRecord): string;
|
|
12
|
+
export declare function resolveTaskResult(task: TaskRecord): unknown;
|
|
13
|
+
export declare function resolveTaskError(task: TaskRecord): unknown;
|
|
14
|
+
export declare function resolveTaskProgress(task: TaskRecord): number | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const STATUS_ALIASES = {
|
|
2
|
+
pending: 'queued',
|
|
3
|
+
queued: 'queued',
|
|
4
|
+
retry_scheduled: 'queued',
|
|
5
|
+
scheduled: 'queued',
|
|
6
|
+
leased: 'processing',
|
|
7
|
+
running: 'processing',
|
|
8
|
+
processing: 'processing',
|
|
9
|
+
active: 'processing',
|
|
10
|
+
completed: 'completed',
|
|
11
|
+
complete: 'completed',
|
|
12
|
+
success: 'completed',
|
|
13
|
+
succeeded: 'completed',
|
|
14
|
+
done: 'completed',
|
|
15
|
+
failed: 'failed',
|
|
16
|
+
error: 'failed',
|
|
17
|
+
dead_lettered: 'failed',
|
|
18
|
+
cancelled: 'cancelled',
|
|
19
|
+
canceled: 'cancelled',
|
|
20
|
+
aborted: 'cancelled',
|
|
21
|
+
};
|
|
22
|
+
export function normalizeTaskStatus(status) {
|
|
23
|
+
const value = String(status ?? '').trim().toLowerCase();
|
|
24
|
+
return STATUS_ALIASES[value] ?? 'unknown';
|
|
25
|
+
}
|
|
26
|
+
export function isTaskActive(task) {
|
|
27
|
+
const lifecycle = normalizeTaskStatus(task.status);
|
|
28
|
+
return lifecycle === 'queued' || lifecycle === 'processing';
|
|
29
|
+
}
|
|
30
|
+
export function canCancelTask(task) {
|
|
31
|
+
return isTaskActive(task);
|
|
32
|
+
}
|
|
33
|
+
export function canRetryTask(task) {
|
|
34
|
+
return normalizeTaskStatus(task.status) === 'failed';
|
|
35
|
+
}
|
|
36
|
+
export function resolveTaskTitle(task) {
|
|
37
|
+
return String(task.title ?? task.name ?? task.id);
|
|
38
|
+
}
|
|
39
|
+
export function resolveTaskMessage(task) {
|
|
40
|
+
return String(task.message ?? task.errorMessage ?? task.error_message ?? '');
|
|
41
|
+
}
|
|
42
|
+
export function resolveTaskUpdatedAt(task) {
|
|
43
|
+
return (task.updatedAt ??
|
|
44
|
+
task.updated_at ??
|
|
45
|
+
task.finishedAt ??
|
|
46
|
+
task.finished_at ??
|
|
47
|
+
task.startedAt ??
|
|
48
|
+
task.started_at ??
|
|
49
|
+
task.createdAt ??
|
|
50
|
+
task.created_at);
|
|
51
|
+
}
|
|
52
|
+
export function resolveTaskCreatedAt(task) {
|
|
53
|
+
return task.createdAt ?? task.created_at;
|
|
54
|
+
}
|
|
55
|
+
export function resolveTaskQueueName(task) {
|
|
56
|
+
return String(task.queueName ?? task.queue_name ?? '');
|
|
57
|
+
}
|
|
58
|
+
export function resolveTaskResult(task) {
|
|
59
|
+
return task.result ?? task.result_data;
|
|
60
|
+
}
|
|
61
|
+
export function resolveTaskError(task) {
|
|
62
|
+
return task.error ?? task.errorMessage ?? task.error_message;
|
|
63
|
+
}
|
|
64
|
+
export function resolveTaskProgress(task) {
|
|
65
|
+
if (typeof task.progress !== 'number' || !Number.isFinite(task.progress)) {
|
|
66
|
+
return normalizeTaskStatus(task.status) === 'completed' ? 100 : undefined;
|
|
67
|
+
}
|
|
68
|
+
return Math.max(0, Math.min(100, Math.round(task.progress)));
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { resolveTaskCreatedAt, resolveTaskError, resolveTaskMessage, resolveTaskResult, resolveTaskUpdatedAt, } from './task-utils';
|
|
3
|
+
describe('task-utils compatibility resolvers', () => {
|
|
4
|
+
test('skips nullable timestamp fields using lifecycle fallback order', () => {
|
|
5
|
+
const task = {
|
|
6
|
+
id: 'task-1',
|
|
7
|
+
updatedAt: null,
|
|
8
|
+
updated_at: null,
|
|
9
|
+
finishedAt: null,
|
|
10
|
+
finished_at: null,
|
|
11
|
+
startedAt: null,
|
|
12
|
+
started_at: '2026-04-26T01:00:00.000Z',
|
|
13
|
+
createdAt: null,
|
|
14
|
+
created_at: '2026-04-26T00:00:00.000Z',
|
|
15
|
+
};
|
|
16
|
+
expect(resolveTaskUpdatedAt(task)).toBe('2026-04-26T01:00:00.000Z');
|
|
17
|
+
expect(resolveTaskCreatedAt(task)).toBe('2026-04-26T00:00:00.000Z');
|
|
18
|
+
});
|
|
19
|
+
test('keeps documented result and error fallback priority with null fields', () => {
|
|
20
|
+
const task = {
|
|
21
|
+
id: 'task-2',
|
|
22
|
+
result: null,
|
|
23
|
+
result_data: { url: 'https://example.test/result.png' },
|
|
24
|
+
error: null,
|
|
25
|
+
errorMessage: null,
|
|
26
|
+
error_message: 'Remote worker failed',
|
|
27
|
+
message: null,
|
|
28
|
+
};
|
|
29
|
+
expect(resolveTaskResult(task)).toEqual({ url: 'https://example.test/result.png' });
|
|
30
|
+
expect(resolveTaskError(task)).toBe('Remote worker failed');
|
|
31
|
+
expect(resolveTaskMessage(task)).toBe('Remote worker failed');
|
|
32
|
+
});
|
|
33
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.3",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"./components/*": "./dist/components/*"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@svadmin/core": "^0.25.
|
|
29
|
+
"@svadmin/core": "^0.25.2",
|
|
30
30
|
"@tanstack/svelte-query": "^6.0.0",
|
|
31
31
|
"highlight.js": "^11.11.1",
|
|
32
32
|
"isomorphic-dompurify": "^3.7.1",
|