@svadmin/ui 0.34.5 → 0.34.7
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/AutoTable.svelte +8 -6
- package/dist/components/fields/MarkdownField.svelte +8 -1
- package/dist/components/task-utils.d.ts +1 -1
- package/dist/components/task-utils.js +2 -1
- package/package.json +8 -5
- package/dist/components/TaskQueueDrawer.test.svelte.d.ts +0 -1
- package/dist/components/TaskQueueDrawer.test.svelte.js +0 -143
- package/dist/components/task-utils.test.d.ts +0 -1
- package/dist/components/task-utils.test.js +0 -33
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
import {
|
|
5
5
|
createTable,
|
|
6
6
|
createCoreRowModel,
|
|
7
|
+
type ColumnDef,
|
|
8
|
+
type SortingState,
|
|
9
|
+
type RowSelectionState,
|
|
10
|
+
type ColumnVisibilityState,
|
|
11
|
+
type ExpandedState,
|
|
12
|
+
} from '@tanstack/svelte-table';
|
|
13
|
+
import {
|
|
7
14
|
column_getCanSort,
|
|
8
15
|
column_getIsSorted,
|
|
9
16
|
column_toggleSorting,
|
|
@@ -21,12 +28,7 @@
|
|
|
21
28
|
row_getIsExpanded,
|
|
22
29
|
row_toggleExpanded,
|
|
23
30
|
cell_getValue,
|
|
24
|
-
|
|
25
|
-
type SortingState,
|
|
26
|
-
type RowSelectionState,
|
|
27
|
-
type ColumnVisibilityState,
|
|
28
|
-
type ExpandedState,
|
|
29
|
-
} from '@tanstack/svelte-table';
|
|
31
|
+
} from '@tanstack/svelte-table/static-functions';
|
|
30
32
|
|
|
31
33
|
import { useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
|
|
32
34
|
import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import * as DOMPurifyPkg from "isomorphic-dompurify";
|
|
2
3
|
interface Props { value: string | null | undefined }
|
|
3
4
|
let { value }: Props = $props();
|
|
5
|
+
|
|
6
|
+
const DOMPurify = 'default' in DOMPurifyPkg ? DOMPurifyPkg.default : DOMPurifyPkg;
|
|
7
|
+
|
|
8
|
+
const sanitizedHtml = $derived(
|
|
9
|
+
value ? DOMPurify.sanitize(value, { USE_PROFILES: { html: true } }) : ''
|
|
10
|
+
);
|
|
4
11
|
</script>
|
|
5
12
|
{#if value}
|
|
6
|
-
<div class="prose prose-sm dark:prose-invert max-w-none leading-relaxed [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-[0.875em] [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_a]:text-primary">{@html
|
|
13
|
+
<div class="prose prose-sm dark:prose-invert max-w-none leading-relaxed [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-[0.875em] [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_a]:text-primary">{@html sanitizedHtml}</div>
|
|
7
14
|
{:else}
|
|
8
15
|
<span class="text-muted-foreground">—</span>
|
|
9
16
|
{/if}
|
|
@@ -5,7 +5,7 @@ export declare function isTaskActive(task: TaskRecord): boolean;
|
|
|
5
5
|
export declare function canCancelTask(task: TaskRecord): boolean;
|
|
6
6
|
export declare function canRetryTask(task: TaskRecord): boolean;
|
|
7
7
|
export declare function resolveTaskTitle(task: TaskRecord): string;
|
|
8
|
-
export declare function resolveTaskMessage(task: TaskRecord): string;
|
|
8
|
+
export declare function resolveTaskMessage(task: TaskRecord): string | undefined;
|
|
9
9
|
export declare function resolveTaskUpdatedAt(task: TaskRecord): unknown;
|
|
10
10
|
export declare function resolveTaskCreatedAt(task: TaskRecord): unknown;
|
|
11
11
|
export declare function resolveTaskQueueName(task: TaskRecord): string;
|
|
@@ -37,7 +37,8 @@ export function resolveTaskTitle(task) {
|
|
|
37
37
|
return String(task.title ?? task.name ?? task.id);
|
|
38
38
|
}
|
|
39
39
|
export function resolveTaskMessage(task) {
|
|
40
|
-
|
|
40
|
+
const message = task.message ?? task.errorMessage ?? task.error_message;
|
|
41
|
+
return message === undefined || message === null ? undefined : String(message);
|
|
41
42
|
}
|
|
42
43
|
export function resolveTaskUpdatedAt(task) {
|
|
43
44
|
return (task.updatedAt ??
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.7",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"./dist/app.css"
|
|
8
8
|
],
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"!dist/**/*.test.*",
|
|
12
|
+
"!dist/**/*.spec.*"
|
|
11
13
|
],
|
|
12
14
|
"main": "dist/index.js",
|
|
13
15
|
"types": "dist/index.d.ts",
|
|
@@ -31,8 +33,8 @@
|
|
|
31
33
|
"./components/*": "./dist/components/*"
|
|
32
34
|
},
|
|
33
35
|
"peerDependencies": {
|
|
34
|
-
"@svadmin/core": "^0.25.
|
|
35
|
-
"@tanstack/svelte-query": "^6.
|
|
36
|
+
"@svadmin/core": "^0.25.4",
|
|
37
|
+
"@tanstack/svelte-query": "^6.1.29",
|
|
36
38
|
"highlight.js": "^11.11.1",
|
|
37
39
|
"isomorphic-dompurify": "^3.7.1",
|
|
38
40
|
"marked": "^17.0.5",
|
|
@@ -67,7 +69,8 @@
|
|
|
67
69
|
},
|
|
68
70
|
"dependencies": {
|
|
69
71
|
"@lucide/svelte": "^1.7.0",
|
|
70
|
-
"@tanstack/svelte-table": "9.0.0-alpha.
|
|
72
|
+
"@tanstack/svelte-table": "9.0.0-alpha.45",
|
|
73
|
+
"@tanstack/table-core": "9.0.0-alpha.43",
|
|
71
74
|
"bits-ui": "^2.17.1",
|
|
72
75
|
"clsx": "^2.1.1",
|
|
73
76
|
"cmdk-sv": "^0.0.19",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
|
3
|
-
import TaskQueueDrawer from './TaskQueueDrawer.svelte';
|
|
4
|
-
const taskFixtures = [
|
|
5
|
-
{
|
|
6
|
-
id: 'task-1',
|
|
7
|
-
name: 'Generate poster',
|
|
8
|
-
status: 'processing',
|
|
9
|
-
message: 'Rendering hero artwork',
|
|
10
|
-
created_at: '2026-04-19T10:00:00.000Z',
|
|
11
|
-
updated_at: '2026-04-19T10:05:00.000Z',
|
|
12
|
-
progress: 42,
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: 'task-2',
|
|
16
|
-
name: 'Send email batch',
|
|
17
|
-
status: 'queued',
|
|
18
|
-
message: 'Waiting for worker',
|
|
19
|
-
created_at: '2026-04-19T10:10:00.000Z',
|
|
20
|
-
updated_at: '2026-04-19T10:12:00.000Z',
|
|
21
|
-
},
|
|
22
|
-
];
|
|
23
|
-
const dlqFixtures = [
|
|
24
|
-
{
|
|
25
|
-
id: 'task-dlq-1',
|
|
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
|
-
},
|
|
33
|
-
];
|
|
34
|
-
const mockTaskProvider = {
|
|
35
|
-
submit: vi.fn(async () => ({
|
|
36
|
-
id: 'submitted-image.generate',
|
|
37
|
-
wait: async () => ({
|
|
38
|
-
id: 'submitted-image.generate',
|
|
39
|
-
name: 'image.generate',
|
|
40
|
-
status: 'queued',
|
|
41
|
-
created_at: '2026-04-19T11:00:00.000Z',
|
|
42
|
-
updated_at: '2026-04-19T11:00:00.000Z',
|
|
43
|
-
}),
|
|
44
|
-
})),
|
|
45
|
-
get: vi.fn(async (taskId) => {
|
|
46
|
-
return [...taskFixtures, ...dlqFixtures].find((task) => task.id === taskId) ?? taskFixtures[0];
|
|
47
|
-
}),
|
|
48
|
-
list: vi.fn(async () => ({ data: taskFixtures, total: taskFixtures.length })),
|
|
49
|
-
listDlq: vi.fn(async () => ({ data: dlqFixtures, total: dlqFixtures.length })),
|
|
50
|
-
retry: vi.fn(async () => undefined),
|
|
51
|
-
cancel: vi.fn(async () => undefined),
|
|
52
|
-
};
|
|
53
|
-
const refetchTasks = vi.fn(async () => ({ data: { data: taskFixtures, total: taskFixtures.length } }));
|
|
54
|
-
const refetchDlq = vi.fn(async () => ({ data: { data: dlqFixtures, total: dlqFixtures.length } }));
|
|
55
|
-
const mutateAsync = vi.fn(async (params) => ({
|
|
56
|
-
id: `submitted-${params.taskName}`,
|
|
57
|
-
wait: async () => ({
|
|
58
|
-
id: `submitted-${params.taskName}`,
|
|
59
|
-
name: params.taskName,
|
|
60
|
-
status: 'queued',
|
|
61
|
-
}),
|
|
62
|
-
}));
|
|
63
|
-
vi.mock('@svadmin/core', () => ({
|
|
64
|
-
getTaskProvider: () => mockTaskProvider,
|
|
65
|
-
useTaskList: ({ dlq }) => ({
|
|
66
|
-
data: dlq
|
|
67
|
-
? { data: dlqFixtures, total: dlqFixtures.length }
|
|
68
|
-
: { data: taskFixtures, total: taskFixtures.length },
|
|
69
|
-
isLoading: false,
|
|
70
|
-
refetch: dlq ? refetchDlq : refetchTasks,
|
|
71
|
-
}),
|
|
72
|
-
useTask: ({ taskId }) => ({
|
|
73
|
-
data: [...taskFixtures, ...dlqFixtures].find((task) => task.id === taskId) ?? taskFixtures[0],
|
|
74
|
-
isLoading: false,
|
|
75
|
-
}),
|
|
76
|
-
useSubmitTask: () => ({
|
|
77
|
-
mutation: {
|
|
78
|
-
mutateAsync,
|
|
79
|
-
isPending: false,
|
|
80
|
-
},
|
|
81
|
-
}),
|
|
82
|
-
}));
|
|
83
|
-
vi.mock('@tanstack/svelte-query', () => ({
|
|
84
|
-
useQueryClient: () => ({
|
|
85
|
-
invalidateQueries: vi.fn(async () => undefined),
|
|
86
|
-
}),
|
|
87
|
-
}));
|
|
88
|
-
describe('TaskQueueDrawer', () => {
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
refetchTasks.mockClear();
|
|
91
|
-
refetchDlq.mockClear();
|
|
92
|
-
mutateAsync.mockClear();
|
|
93
|
-
mockTaskProvider.submit.mockClear();
|
|
94
|
-
mockTaskProvider.get.mockClear();
|
|
95
|
-
mockTaskProvider.list.mockClear();
|
|
96
|
-
mockTaskProvider.listDlq.mockClear();
|
|
97
|
-
mockTaskProvider.retry.mockClear();
|
|
98
|
-
mockTaskProvider.cancel.mockClear();
|
|
99
|
-
});
|
|
100
|
-
it('supports tab switching, filtering, and detail selection', async () => {
|
|
101
|
-
render(TaskQueueDrawer, { open: true, taskProvider: mockTaskProvider });
|
|
102
|
-
expect(screen.getByText('Task Center')).toBeTruthy();
|
|
103
|
-
expect(screen.getAllByText('Generate poster').length).toBeGreaterThan(0);
|
|
104
|
-
expect(screen.getAllByText('Rendering hero artwork').length).toBeGreaterThan(0);
|
|
105
|
-
const searchInput = screen.getByPlaceholderText('Filter tasks by name, ID, status, or message...');
|
|
106
|
-
await fireEvent.input(searchInput, { target: { value: 'email' } });
|
|
107
|
-
await waitFor(() => {
|
|
108
|
-
expect(screen.queryAllByText('Generate poster').length).toBeLessThan(2);
|
|
109
|
-
});
|
|
110
|
-
expect(screen.getByText('Send email batch')).toBeTruthy();
|
|
111
|
-
await fireEvent.input(searchInput, { target: { value: '' } });
|
|
112
|
-
await fireEvent.click(screen.getByRole('tab', { name: /DLQ/i }));
|
|
113
|
-
expect(screen.getByText('Nightly sync')).toBeTruthy();
|
|
114
|
-
const dlqRow = screen.getByText('Nightly sync').closest('tr');
|
|
115
|
-
expect(dlqRow).toBeTruthy();
|
|
116
|
-
await fireEvent.click(dlqRow);
|
|
117
|
-
await waitFor(() => {
|
|
118
|
-
expect(screen.getAllByText('Remote API timeout').length).toBeGreaterThan(0);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
it('submits a task from the inline form', async () => {
|
|
122
|
-
render(TaskQueueDrawer, { open: true, taskProvider: mockTaskProvider });
|
|
123
|
-
await fireEvent.click(screen.getAllByRole('button', { name: 'Submit Task' })[0]);
|
|
124
|
-
await fireEvent.input(screen.getByLabelText('Task name'), {
|
|
125
|
-
target: { value: 'image.generate' },
|
|
126
|
-
});
|
|
127
|
-
await fireEvent.input(screen.getByLabelText('Task payload (JSON)'), {
|
|
128
|
-
target: { value: '{"prompt":"poster"}' },
|
|
129
|
-
});
|
|
130
|
-
const submitButtons = screen.getAllByRole('button', { name: 'Submit Task' });
|
|
131
|
-
await fireEvent.click(submitButtons[submitButtons.length - 1]);
|
|
132
|
-
await waitFor(() => {
|
|
133
|
-
expect(mutateAsync).toHaveBeenCalledWith({
|
|
134
|
-
taskName: 'image.generate',
|
|
135
|
-
taskProvider: mockTaskProvider,
|
|
136
|
-
options: {
|
|
137
|
-
body: { prompt: 'poster' },
|
|
138
|
-
idempotencyKey: undefined,
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
});
|