@svadmin/ui 0.33.0 → 0.34.0

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.
@@ -27,6 +27,8 @@ export interface ComponentRegistry {
27
27
  UserMenu?: Component<any>;
28
28
  /** Custom notification panel / bell icon in the header */
29
29
  NotificationPanel?: Component<any>;
30
+ /** Custom task queue drawer / task center trigger in the header */
31
+ TaskQueueDrawer?: Component<any>;
30
32
  }
31
33
  /** Set the component registry in context (called by AdminApp). */
32
34
  export declare function setComponentRegistry(registry: ComponentRegistry): void;
@@ -2,8 +2,8 @@
2
2
  // Auto-inject design tokens so consumers don't need to manually import
3
3
  import '../app.css';
4
4
  import type { Snippet } from 'svelte';
5
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem } from '@svadmin/core';
6
- import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider, configureTheme } from '@svadmin/core';
5
+ import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
6
+ import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, setTaskProvider, getAuthProvider, createHashRouterProvider, configureTheme } from '@svadmin/core';
7
7
  import { t } from '@svadmin/core/i18n';
8
8
  import { navigate } from '@svadmin/core/router';
9
9
  import { currentPath } from '@svadmin/core/router';
@@ -22,6 +22,7 @@
22
22
  import DevTools from './DevTools.svelte';
23
23
  import Sidebar from './Sidebar.svelte';
24
24
  import Header from './Header.svelte';
25
+ import TaskQueueDrawer from './TaskQueueDrawer.svelte';
25
26
  import SettingsPage from './SettingsPage.svelte';
26
27
  import { Button } from './ui/button/index.js';
27
28
  import { Input } from './ui/input/index.js';
@@ -32,6 +33,7 @@
32
33
  interface Props {
33
34
  dataProvider: DataProvider;
34
35
  authProvider?: AuthProvider;
36
+ taskProvider?: TaskProvider;
35
37
  routerProvider?: RouterProvider;
36
38
  resources: ResourceDefinition[];
37
39
  locale?: string;
@@ -54,6 +56,7 @@
54
56
  let {
55
57
  dataProvider,
56
58
  authProvider,
59
+ taskProvider,
57
60
  routerProvider,
58
61
  resources,
59
62
  locale,
@@ -72,6 +75,7 @@
72
75
  const defaultComponents: ComponentRegistry = {
73
76
  Layout, Sidebar, Header,
74
77
  LoginPage, AutoTable, AutoForm, ShowPage,
78
+ TaskQueueDrawer,
75
79
  Button: Button as unknown as ComponentRegistry['Button'],
76
80
  Input: Input as unknown as ComponentRegistry['Input'],
77
81
  Badge: Badge as unknown as ComponentRegistry['Badge'],
@@ -90,6 +94,7 @@
90
94
  // Set up initial context synchronously so children can access resources immediately during first render
91
95
  setDataProvider(dataProvider);
92
96
  if (authProvider) setAuthProvider(authProvider);
97
+ if (taskProvider) setTaskProvider(taskProvider);
93
98
  setResources(resources);
94
99
  setRouterProvider(resolvedRouter);
95
100
  if (locale) setLocale(locale);
@@ -100,6 +105,7 @@
100
105
  $effect.pre(() => {
101
106
  setDataProvider(dataProvider);
102
107
  if (authProvider) setAuthProvider(authProvider);
108
+ if (taskProvider) setTaskProvider(taskProvider);
103
109
  setResources(resources);
104
110
  setRouterProvider(resolvedRouter);
105
111
  if (locale) setLocale(locale);
@@ -1,10 +1,11 @@
1
1
  import '../app.css';
2
2
  import type { Snippet } from 'svelte';
3
- import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem } from '@svadmin/core';
3
+ import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
4
4
  import { type ComponentRegistry } from '../component-registry.svelte.js';
5
5
  interface Props {
6
6
  dataProvider: DataProvider;
7
7
  authProvider?: AuthProvider;
8
+ taskProvider?: TaskProvider;
8
9
  routerProvider?: RouterProvider;
9
10
  resources: ResourceDefinition[];
10
11
  locale?: string;
@@ -3,19 +3,30 @@
3
3
  import { cn } from '../utils.js';
4
4
  import {
5
5
  createTable,
6
+ createCoreRowModel,
7
+ column_getCanSort,
8
+ column_getIsSorted,
9
+ column_toggleSorting,
10
+ column_getIsVisible,
11
+ column_toggleVisibility,
12
+ header_getSize,
13
+ table_getRowModel,
14
+ table_getHeaderGroups,
15
+ table_getAllLeafColumns,
16
+ table_getIsAllRowsSelected,
17
+ table_toggleAllRowsSelected,
18
+ row_getIsSelected,
19
+ row_toggleSelected,
20
+ row_getAllVisibleCells,
21
+ row_getIsExpanded,
22
+ row_toggleExpanded,
23
+ cell_getValue,
6
24
  type ColumnDef,
7
25
  type SortingState,
8
26
  type RowSelectionState,
9
27
  type ColumnVisibilityState,
10
28
  type ExpandedState,
11
29
  } from '@tanstack/svelte-table';
12
- import {
13
- createCoreRowModel,
14
- column_getCanSort,
15
- column_getIsSorted,
16
- column_toggleSorting,
17
- header_getSize
18
- } from '@tanstack/table-core';
19
30
 
20
31
  import { useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
21
32
  import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
@@ -356,7 +367,7 @@
356
367
  confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
357
368
  confirmAction = async () => {
358
369
  try {
359
- await deleteManyMutation.mutateAsync({ ids, resource: resourceName, successNotification: false });
370
+ await deleteManyMutation.mutateAsync({ ids, resource: resourceName });
360
371
  notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: ids.length }) });
361
372
  } catch {
362
373
  notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
@@ -429,10 +440,10 @@
429
440
  {/snippet}
430
441
  </DropdownMenu.Trigger>
431
442
  <DropdownMenu.Content align="end" class="w-48">
432
- {#each tbl.getAllLeafColumns().filter((c: any) => !c.id.startsWith('_')) as column}
443
+ {#each table_getAllLeafColumns(tbl).filter((c: any) => !c.id.startsWith('_')) as column}
433
444
  <DropdownMenu.CheckboxItem
434
- checked={column.getIsVisible()}
435
- onCheckedChange={(v) => column.toggleVisibility(!!v)}
445
+ checked={column_getIsVisible(column)}
446
+ onCheckedChange={(v) => column_toggleVisibility(column, !!v)}
436
447
  >
437
448
  {visibleFields.find(f => f.key === column.id)?.label ?? column.id}
438
449
  </DropdownMenu.CheckboxItem>
@@ -552,7 +563,7 @@
552
563
  <div class="hidden md:block">
553
564
  <Table.Root>
554
565
  <Table.Header>
555
- {#each tbl.getHeaderGroups() as headerGroup}
566
+ {#each table_getHeaderGroups(tbl) as headerGroup}
556
567
  <DraggableHeader
557
568
  columns={headerGroup.headers.map((h: any) => ({ id: h.column.id, header: h }))}
558
569
  resourceName={resourceName}
@@ -567,8 +578,8 @@
567
578
  >
568
579
  {#if header.id === '_select'}
569
580
  <Checkbox
570
- checked={tbl.getIsAllRowsSelected()}
571
- onCheckedChange={() => tbl.toggleAllRowsSelected()}
581
+ checked={table_getIsAllRowsSelected(tbl)}
582
+ onCheckedChange={() => table_toggleAllRowsSelected(tbl)}
572
583
  />
573
584
  {:else if header.id === '_expand'}
574
585
  <!-- empty -->
@@ -598,23 +609,23 @@
598
609
  {/each}
599
610
  </Table.Header>
600
611
  <Table.Body>
601
- {#each tbl.getRowModel().rows as row}
612
+ {#each table_getRowModel(tbl).rows as row}
602
613
  {@const record = row.original}
603
614
  {@const id = record[primaryKey] as string | number}
604
615
  <ContextMenu.Root>
605
616
  <ContextMenu.Trigger>
606
617
  {#snippet child({ props })}
607
- <Table.Row {...props} class="transition-all duration-300 border-b border-border/10 {row.getIsSelected() ? 'bg-primary/5' : 'hover:bg-muted/20'}">
608
- {#each row.getVisibleCells() as cell}
618
+ <Table.Row {...props} class="transition-all duration-300 border-b border-border/10 {row_getIsSelected(row) ? 'bg-primary/5' : 'hover:bg-muted/20'}">
619
+ {#each row_getAllVisibleCells(row) as cell}
609
620
  <Table.Cell>
610
621
  {#if cell.column.id === '_select'}
611
622
  <Checkbox
612
- checked={row.getIsSelected()}
613
- onCheckedChange={() => row.toggleSelected()}
623
+ checked={row_getIsSelected(row)}
624
+ onCheckedChange={() => row_toggleSelected(row)}
614
625
  />
615
626
  {:else if cell.column.id === '_expand'}
616
- <TooltipButton tooltip={row.getIsExpanded() ? t('common.collapse') : t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row.toggleExpanded()}>
617
- {#if row.getIsExpanded()}
627
+ <TooltipButton tooltip={row_getIsExpanded(row) ? t('common.collapse') : t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row_toggleExpanded(row)}>
628
+ {#if row_getIsExpanded(row)}
618
629
  <ChevronUp class="h-4 w-4" />
619
630
  {:else}
620
631
  <ChevronDown class="h-4 w-4" />
@@ -640,34 +651,34 @@
640
651
  {:else}
641
652
  {@const field = visibleFields.find(f => f.key === cell.column.id)}
642
653
  {#if customColumns && field && customColumns[field.key]}
643
- {@render customColumns[field.key]({ value: cell.getValue(), record })}
654
+ {@render customColumns[field.key]({ value: cell_getValue(cell), record })}
644
655
  {:else if defaultCellRenderer && field}
645
- {@render defaultCellRenderer({ field, value: cell.getValue(), record })}
656
+ {@render defaultCellRenderer({ field, value: cell_getValue(cell), record })}
646
657
  {:else if field?.type === 'boolean'}
647
- <span class="inline-block h-2 w-2 rounded-full {cell.getValue() ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
648
- {:else if field?.type === 'date' && cell.getValue()}
649
- {new Date(cell.getValue() as string).toLocaleDateString()}
650
- {:else if field?.type === 'tags' && Array.isArray(cell.getValue())}
658
+ <span class="inline-block h-2 w-2 rounded-full {cell_getValue(cell) ? 'bg-success' : 'bg-muted-foreground/30'}"></span>
659
+ {:else if field?.type === 'date' && cell_getValue(cell)}
660
+ {new Date(cell_getValue(cell) as string).toLocaleDateString()}
661
+ {:else if field?.type === 'tags' && Array.isArray(cell_getValue(cell))}
651
662
  <div class="flex flex-wrap gap-1">
652
- {#each (cell.getValue() as string[]).slice(0, 3) as tag}
663
+ {#each (cell_getValue(cell) as string[]).slice(0, 3) as tag}
653
664
  <Badge variant="secondary">{tag}</Badge>
654
665
  {/each}
655
666
  </div>
656
667
  {:else if field?.type === 'select' && field.options}
657
- {@const opt = field.options.find(o => o.value === cell.getValue())}
658
- <Badge variant="outline">{opt?.label ?? cell.getValue() ?? '—'}</Badge>
668
+ {@const opt = field.options.find(o => o.value === cell_getValue(cell))}
669
+ <Badge variant="outline">{opt?.label ?? cell_getValue(cell) ?? '—'}</Badge>
659
670
  {:else if canEdit && field && ['text', 'number', 'email', 'url'].includes(field.type)}
660
671
  <InlineEdit
661
672
  {resourceName}
662
673
  recordId={id}
663
674
  {field}
664
- value={cell.getValue()}
675
+ value={cell_getValue(cell)}
665
676
  onSave={() => listResult.refetch()}
666
677
  />
667
678
  {:else if field?.key === primaryKey}
668
- <span class="font-mono text-xs">{cell.getValue() ?? '—'}</span>
679
+ <span class="font-mono text-xs">{cell_getValue(cell) ?? '—'}</span>
669
680
  {:else}
670
- {cell.getValue() ?? '—'}
681
+ {cell_getValue(cell) ?? '—'}
671
682
  {/if}
672
683
  {/if}
673
684
  </Table.Cell>
@@ -695,9 +706,9 @@
695
706
  {/if}
696
707
  </ContextMenu.Content>
697
708
  </ContextMenu.Root>
698
- {#if expandedRowRender && row.getIsExpanded()}
709
+ {#if expandedRowRender && row_getIsExpanded(row)}
699
710
  <Table.Row class="bg-muted/10 border-b border-border/10 transition-all">
700
- <Table.Cell colspan={row.getVisibleCells().length}>
711
+ <Table.Cell colspan={row_getAllVisibleCells(row).length}>
701
712
  {@render expandedRowRender({ record })}
702
713
  </Table.Cell>
703
714
  </Table.Row>
@@ -731,19 +742,19 @@
731
742
 
732
743
  <!-- Mobile Card View (visible only on small screens) -->
733
744
  <div class="md:hidden space-y-3 p-2">
734
- {#each tbl.getRowModel().rows as row}
745
+ {#each table_getRowModel(tbl).rows as row}
735
746
  {@const record = row.original}
736
747
  {@const id = record[primaryKey] as string | number}
737
748
  <div
738
- class="rounded-[20px] shadow-[0_4px_20px_rgb(0,0,0,0.03)] ring-1 ring-border/20 bg-card p-5 transition-all {row.getIsSelected() ? 'ring-2 ring-primary/50 bg-primary/5' : ''}"
749
+ class="rounded-[20px] shadow-[0_4px_20px_rgb(0,0,0,0.03)] ring-1 ring-border/20 bg-card p-5 transition-all {row_getIsSelected(row) ? 'ring-2 ring-primary/50 bg-primary/5' : ''}"
739
750
  >
740
751
  <!-- Card header: ID + select + actions -->
741
752
  <div class="flex items-center justify-between mb-3">
742
753
  <div class="flex items-center gap-2">
743
754
  {#if selectable && (canDelete || batchActions)}
744
755
  <Checkbox
745
- checked={row.getIsSelected()}
746
- onCheckedChange={() => row.toggleSelected()}
756
+ checked={row_getIsSelected(row)}
757
+ onCheckedChange={() => row_toggleSelected(row)}
747
758
  />
748
759
  {/if}
749
760
  <span class="text-xs font-mono text-muted-foreground">#{id}</span>
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { getTaskProvider } from '@svadmin/core';
3
+ import { useQueryClient } from '@tanstack/svelte-query';
4
+ import { t } from '@svadmin/core/i18n';
5
+ import { Button } from './ui/button/index.js';
6
+ import { Loader2, Ban } from '@lucide/svelte';
7
+
8
+ let {
9
+ taskId,
10
+ taskProvider = getTaskProvider({ optional: true }),
11
+ onSuccess,
12
+ onError,
13
+ disabled = false,
14
+ variant = 'outline',
15
+ size = 'sm',
16
+ children,
17
+ } = $props<{
18
+ taskId: string;
19
+ taskProvider?: import('@svadmin/core').TaskProvider;
20
+ onSuccess?: () => void;
21
+ onError?: (error: unknown) => void;
22
+ disabled?: boolean;
23
+ variant?: import('./ui/button/index.js').ButtonVariant;
24
+ size?: import('./ui/button/index.js').ButtonSize;
25
+ children?: import('svelte').Snippet;
26
+ }>();
27
+
28
+ const queryClient = useQueryClient();
29
+ let pending = $state(false);
30
+
31
+ async function handleCancel() {
32
+ if (!taskProvider?.cancel || pending) return;
33
+ pending = true;
34
+ try {
35
+ await taskProvider.cancel(taskId);
36
+ await Promise.all([
37
+ queryClient.invalidateQueries({ queryKey: ['taskList'] }),
38
+ queryClient.invalidateQueries({ queryKey: ['task', taskId] }),
39
+ ]);
40
+ onSuccess?.();
41
+ } catch (error) {
42
+ onError?.(error);
43
+ } finally {
44
+ pending = false;
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <Button {variant} {size} onclick={handleCancel} disabled={disabled || pending || !taskProvider?.cancel}>
50
+ {#if pending}
51
+ <Loader2 class="mr-1.5 h-3.5 w-3.5 animate-spin" />
52
+ {:else}
53
+ <Ban class="mr-1.5 h-3.5 w-3.5" />
54
+ {/if}
55
+ {#if children}
56
+ {@render children()}
57
+ {:else}
58
+ {t('common.cancel')}
59
+ {/if}
60
+ </Button>
@@ -0,0 +1,13 @@
1
+ type $$ComponentProps = {
2
+ taskId: string;
3
+ taskProvider?: import('@svadmin/core').TaskProvider;
4
+ onSuccess?: () => void;
5
+ onError?: (error: unknown) => void;
6
+ disabled?: boolean;
7
+ variant?: import('./ui/button/index.js').ButtonVariant;
8
+ size?: import('./ui/button/index.js').ButtonSize;
9
+ children?: import('svelte').Snippet;
10
+ };
11
+ declare const CancelTaskButton: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type CancelTaskButton = ReturnType<typeof CancelTaskButton>;
13
+ export default CancelTaskButton;
@@ -8,14 +8,15 @@
8
8
  import ChatDialog from './ChatDialog.svelte';
9
9
  import TooltipButton from './TooltipButton.svelte';
10
10
  import { t } from '@svadmin/core/i18n';
11
- import { getAuthProvider } from '@svadmin/core';
12
- import type { Identity, MenuItem } from '@svadmin/core';
11
+ import { getAuthProvider, getTaskProvider } from '@svadmin/core';
12
+ import type { Identity, MenuItem, TaskProvider, TaskRecord } from '@svadmin/core';
13
13
  import { navigate } from '@svadmin/core/router';
14
14
  import { getPath } from '../router-state.svelte.js';
15
15
  import { Skeleton } from './ui/skeleton/index.js';
16
16
  import { Button } from './ui/button/index.js';
17
17
  import * as Sheet from './ui/sheet/index.js';
18
18
  import { Menu } from '@lucide/svelte';
19
+ import { getComponentRegistry } from '../component-registry.svelte.js';
19
20
 
20
21
  let commandOpen = $state(false);
21
22
  let shortcutsOpen = $state(false);
@@ -32,6 +33,9 @@
32
33
  }
33
34
  let loading = $state(true);
34
35
  let identity = $state<Identity | null>(null);
36
+ const taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined;
37
+ const registry = getComponentRegistry() || {} as any;
38
+ const TaskQueueComponent = registry.TaskQueueDrawer;
35
39
 
36
40
  $effect(() => {
37
41
  let cancelled = false;
@@ -149,6 +153,13 @@
149
153
  <Menu class="h-5 w-5" />
150
154
  </TooltipButton>
151
155
  {/snippet}
156
+ {#snippet rightActions()}
157
+ {#if taskProvider}
158
+ {#if TaskQueueComponent}
159
+ <TaskQueueComponent {taskProvider} />
160
+ {/if}
161
+ {/if}
162
+ {/snippet}
152
163
  </Header>
153
164
 
154
165
  <!-- Content area -->
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { getTaskProvider } from '@svadmin/core';
3
+ import { useQueryClient } from '@tanstack/svelte-query';
4
+ import { t } from '@svadmin/core/i18n';
5
+ import { Button } from './ui/button/index.js';
6
+ import { Loader2, RotateCcw } from '@lucide/svelte';
7
+
8
+ let {
9
+ taskId,
10
+ taskProvider = getTaskProvider({ optional: true }),
11
+ onSuccess,
12
+ onError,
13
+ disabled = false,
14
+ variant = 'outline',
15
+ size = 'sm',
16
+ children,
17
+ } = $props<{
18
+ taskId: string;
19
+ taskProvider?: import('@svadmin/core').TaskProvider;
20
+ onSuccess?: () => void;
21
+ onError?: (error: unknown) => void;
22
+ disabled?: boolean;
23
+ variant?: import('./ui/button/index.js').ButtonVariant;
24
+ size?: import('./ui/button/index.js').ButtonSize;
25
+ children?: import('svelte').Snippet;
26
+ }>();
27
+
28
+ const queryClient = useQueryClient();
29
+ let pending = $state(false);
30
+
31
+ async function handleRetry() {
32
+ if (!taskProvider?.retry || pending) return;
33
+ pending = true;
34
+ try {
35
+ await taskProvider.retry(taskId);
36
+ await Promise.all([
37
+ queryClient.invalidateQueries({ queryKey: ['taskList'] }),
38
+ queryClient.invalidateQueries({ queryKey: ['task', taskId] }),
39
+ ]);
40
+ onSuccess?.();
41
+ } catch (error) {
42
+ onError?.(error);
43
+ } finally {
44
+ pending = false;
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <Button {variant} {size} onclick={handleRetry} disabled={disabled || pending || !taskProvider?.retry}>
50
+ {#if pending}
51
+ <Loader2 class="mr-1.5 h-3.5 w-3.5 animate-spin" />
52
+ {:else}
53
+ <RotateCcw class="mr-1.5 h-3.5 w-3.5" />
54
+ {/if}
55
+ {#if children}
56
+ {@render children()}
57
+ {:else}
58
+ {t('common.retry')}
59
+ {/if}
60
+ </Button>
@@ -0,0 +1,13 @@
1
+ type $$ComponentProps = {
2
+ taskId: string;
3
+ taskProvider?: import('@svadmin/core').TaskProvider;
4
+ onSuccess?: () => void;
5
+ onError?: (error: unknown) => void;
6
+ disabled?: boolean;
7
+ variant?: import('./ui/button/index.js').ButtonVariant;
8
+ size?: import('./ui/button/index.js').ButtonSize;
9
+ children?: import('svelte').Snippet;
10
+ };
11
+ declare const RetryTaskButton: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type RetryTaskButton = ReturnType<typeof RetryTaskButton>;
13
+ export default RetryTaskButton;
@@ -0,0 +1,138 @@
1
+ <script lang="ts">
2
+ import { useTask, getTaskProvider } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import type { TaskProvider, TaskRecord } from '@svadmin/core';
5
+ import { Loader2 } from '@lucide/svelte';
6
+ import * as Card from './ui/card/index.js';
7
+ import { Badge } from './ui/badge/index.js';
8
+ import TaskStatusBadge from './TaskStatusBadge.svelte';
9
+ import TaskProgressBar from './TaskProgressBar.svelte';
10
+ import RetryTaskButton from './RetryTaskButton.svelte';
11
+ import CancelTaskButton from './CancelTaskButton.svelte';
12
+
13
+ let {
14
+ task,
15
+ taskId,
16
+ taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined,
17
+ title,
18
+ useProviderData = !task && !!taskId,
19
+ queryOptions,
20
+ } = $props<{
21
+ task?: TaskRecord;
22
+ taskId?: string;
23
+ taskProvider?: TaskProvider<TaskRecord>;
24
+ title?: string;
25
+ useProviderData?: boolean;
26
+ queryOptions?: {
27
+ enabled?: boolean;
28
+ staleTime?: number;
29
+ gcTime?: number;
30
+ refetchOnWindowFocus?: boolean;
31
+ refetchInterval?: number | false;
32
+ refetchIntervalInBackground?: boolean;
33
+ };
34
+ }>();
35
+
36
+ const query = useTask({
37
+ get taskId() {
38
+ return taskId;
39
+ },
40
+ get taskProvider() {
41
+ return taskProvider;
42
+ },
43
+ get queryOptions() {
44
+ return { enabled: useProviderData && !!taskId && !!taskProvider, ...queryOptions };
45
+ },
46
+ });
47
+
48
+ const resolvedTask = $derived(task ?? query.data);
49
+ const resolvedTitle = $derived(title ?? t('task.detailsTitle'));
50
+
51
+ function formatDate(value: unknown) {
52
+ if (!value) return '—';
53
+ return new Date(value as string | Date).toLocaleString();
54
+ }
55
+
56
+ function toPrettyJson(value: unknown) {
57
+ try {
58
+ return JSON.stringify(value, null, 2);
59
+ } catch {
60
+ return String(value);
61
+ }
62
+ }
63
+ </script>
64
+
65
+ <Card.Root class="border-border/40 shadow-sm">
66
+ <Card.Header class="pb-3">
67
+ <Card.Title class="text-base">{resolvedTitle}</Card.Title>
68
+ <Card.Description>
69
+ {#if resolvedTask}
70
+ {String(resolvedTask.title ?? resolvedTask.name ?? resolvedTask.id)}
71
+ {:else}
72
+ {t('task.detailsSummary')}
73
+ {/if}
74
+ </Card.Description>
75
+ </Card.Header>
76
+ <Card.Content class="space-y-5 pt-0">
77
+ {#if useProviderData && taskProvider && query.isLoading}
78
+ <div class="flex h-40 items-center justify-center text-muted-foreground">
79
+ <Loader2 class="mr-2 h-4 w-4 animate-spin" />
80
+ {t('task.loadingDetail')}
81
+ </div>
82
+ {:else if resolvedTask}
83
+ <div class="flex flex-wrap items-center gap-2">
84
+ <TaskStatusBadge status={String(resolvedTask.status ?? 'pending')} />
85
+ <Badge variant="outline" class="font-mono text-xs">{resolvedTask.id}</Badge>
86
+ </div>
87
+
88
+ {#if typeof resolvedTask.progress === 'number'}
89
+ <TaskProgressBar value={Number(resolvedTask.progress)} />
90
+ {/if}
91
+
92
+ <div class="grid gap-4 md:grid-cols-2">
93
+ <div class="space-y-1">
94
+ <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.createdLabel')}</div>
95
+ <div class="text-sm">{formatDate(resolvedTask.createdAt)}</div>
96
+ </div>
97
+ <div class="space-y-1">
98
+ <div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.updatedLabel')}</div>
99
+ <div class="text-sm">{formatDate(resolvedTask.updatedAt)}</div>
100
+ </div>
101
+ </div>
102
+
103
+ {#if resolvedTask.message}
104
+ <div class="space-y-1">
105
+ <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">{String(resolvedTask.message)}</div>
107
+ </div>
108
+ {/if}
109
+
110
+ {#if resolvedTask.error}
111
+ <div class="space-y-1">
112
+ <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.error)}</pre>
114
+ </div>
115
+ {/if}
116
+
117
+ <div class="space-y-1">
118
+ <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>
120
+ </div>
121
+
122
+ {#if taskProvider?.retry || taskProvider?.cancel}
123
+ <div class="flex flex-wrap gap-2 pt-2">
124
+ {#if taskProvider?.retry && ['failed', 'error'].includes(String(resolvedTask.status).toLowerCase())}
125
+ <RetryTaskButton taskId={resolvedTask.id} {taskProvider} />
126
+ {/if}
127
+ {#if taskProvider?.cancel && ['pending', 'queued', 'running', 'processing'].includes(String(resolvedTask.status).toLowerCase())}
128
+ <CancelTaskButton taskId={resolvedTask.id} {taskProvider} />
129
+ {/if}
130
+ </div>
131
+ {/if}
132
+ {:else}
133
+ <div class="flex h-32 items-center justify-center text-sm text-muted-foreground">
134
+ {t('task.noSelection')}
135
+ </div>
136
+ {/if}
137
+ </Card.Content>
138
+ </Card.Root>
@@ -0,0 +1,19 @@
1
+ import type { TaskProvider, TaskRecord } from '@svadmin/core';
2
+ type $$ComponentProps = {
3
+ task?: TaskRecord;
4
+ taskId?: string;
5
+ taskProvider?: TaskProvider<TaskRecord>;
6
+ title?: string;
7
+ useProviderData?: boolean;
8
+ queryOptions?: {
9
+ enabled?: boolean;
10
+ staleTime?: number;
11
+ gcTime?: number;
12
+ refetchOnWindowFocus?: boolean;
13
+ refetchInterval?: number | false;
14
+ refetchIntervalInBackground?: boolean;
15
+ };
16
+ };
17
+ declare const TaskDetails: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ type TaskDetails = ReturnType<typeof TaskDetails>;
19
+ export default TaskDetails;