@svadmin/ui 0.70.0 → 0.72.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.
Files changed (158) hide show
  1. package/dist/actions.js +1 -1
  2. package/dist/app.css +102 -63
  3. package/dist/app.theme.css +102 -63
  4. package/dist/component-registry.svelte.d.ts +33 -19
  5. package/dist/component-registry.svelte.js +6 -4
  6. package/dist/components/AdminApp.svelte +55 -53
  7. package/dist/components/AdminApp.svelte.d.ts +2 -2
  8. package/dist/components/ApiSettings.svelte +19 -1
  9. package/dist/components/AuditLogDrawer.svelte +14 -3
  10. package/dist/components/AutoForm.svelte +146 -48
  11. package/dist/components/AutoForm.svelte.d.ts +2 -0
  12. package/dist/components/AutoTable.svelte +358 -258
  13. package/dist/components/AutoTable.svelte.d.ts +1 -0
  14. package/dist/components/BoundRecordDetailDrawer.svelte +166 -0
  15. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +9 -0
  16. package/dist/components/Breadcrumbs.svelte +7 -4
  17. package/dist/components/CancelTaskButton.svelte +9 -5
  18. package/dist/components/ColumnHeaderFilter.svelte +4 -3
  19. package/dist/components/ColumnSettings.svelte +1 -0
  20. package/dist/components/ComboboxField.svelte +90 -30
  21. package/dist/components/ComboboxField.svelte.d.ts +2 -2
  22. package/dist/components/CreatePage.svelte +3 -1
  23. package/dist/components/DetailDrawer.svelte +4 -0
  24. package/dist/components/DetailDrawer.svelte.d.ts +1 -0
  25. package/dist/components/DevTools.svelte +6 -19
  26. package/dist/components/DraggableGrid.svelte +1 -0
  27. package/dist/components/DraggableHeader.svelte +5 -9
  28. package/dist/components/DraggableHeader.svelte.d.ts +43 -18
  29. package/dist/components/DraggableRowTable.svelte +2 -2
  30. package/dist/components/DynamicFormList.svelte +17 -17
  31. package/dist/components/DynamicFormList.svelte.d.ts +2 -1
  32. package/dist/components/EditPage.svelte +3 -1
  33. package/dist/components/EditableTable.svelte +4 -0
  34. package/dist/components/ErrorPage.svelte +3 -1
  35. package/dist/components/FieldDisplay.svelte +14 -0
  36. package/dist/components/FieldDisplay.svelte.d.ts +7 -0
  37. package/dist/components/FieldRenderer.svelte +9 -5
  38. package/dist/components/FilterBuilder.svelte +19 -8
  39. package/dist/components/FilterDropdown.svelte.d.ts +1 -1
  40. package/dist/components/Header.svelte +5 -6
  41. package/dist/components/ImportWizard.svelte +3 -2
  42. package/dist/components/InfiniteList.svelte +22 -8
  43. package/dist/components/InfiniteList.svelte.d.ts +5 -3
  44. package/dist/components/InlineEdit.svelte +114 -44
  45. package/dist/components/JsonSchemaForm.svelte +19 -14
  46. package/dist/components/Layout.svelte +19 -15
  47. package/dist/components/LazyTaskQueueDrawer.svelte +26 -0
  48. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +8 -0
  49. package/dist/components/ListPage.svelte +7 -6
  50. package/dist/components/MasterDetailView.svelte +3 -3
  51. package/dist/components/MediaLibraryModal.svelte +5 -2
  52. package/dist/components/MentionsInput.svelte +4 -3
  53. package/dist/components/NavigateToResource.svelte +3 -2
  54. package/dist/components/PermissionMatrix.svelte +4 -3
  55. package/dist/components/ProfilePage.svelte +2 -2
  56. package/dist/components/QuickEditDrawer.svelte +39 -0
  57. package/dist/components/QuickEditDrawer.svelte.d.ts +8 -0
  58. package/dist/components/RecordDetailDrawer.svelte +14 -108
  59. package/dist/components/RecordRowActions.svelte +41 -0
  60. package/dist/components/RecordRowActions.svelte.d.ts +14 -0
  61. package/dist/components/ResourceAccessGuard.svelte +7 -5
  62. package/dist/components/RetryTaskButton.svelte +9 -5
  63. package/dist/components/RowActions.svelte +5 -3
  64. package/dist/components/SensitiveDataMask.svelte +3 -3
  65. package/dist/components/SettingsPage.svelte +1 -1
  66. package/dist/components/ShowPage.svelte +44 -38
  67. package/dist/components/Sidebar.svelte +10 -8
  68. package/dist/components/SplitPaneLayout.svelte +4 -2
  69. package/dist/components/StepsForm.svelte +161 -44
  70. package/dist/components/StepsForm.svelte.d.ts +1 -0
  71. package/dist/components/TaskDetails.svelte +20 -7
  72. package/dist/components/TaskList.svelte +19 -7
  73. package/dist/components/TaskQueueDrawer.svelte +21 -22
  74. package/dist/components/Toast.svelte +4 -2
  75. package/dist/components/TooltipButton.svelte +1 -1
  76. package/dist/components/TreeSelect.svelte +4 -2
  77. package/dist/components/ValidatedField.svelte +27 -0
  78. package/dist/components/ValidatedField.svelte.d.ts +35 -0
  79. package/dist/components/account/ImportMembersPage.svelte +2 -2
  80. package/dist/components/account/SecurityLogPage.svelte +3 -1
  81. package/dist/components/account/SettingsEnterprisePage.svelte +7 -5
  82. package/dist/components/account/TeamMembersPage.svelte +1 -1
  83. package/dist/components/buttons/CloneButton.svelte +6 -4
  84. package/dist/components/buttons/DeleteButton.svelte +95 -29
  85. package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -0
  86. package/dist/components/buttons/EditButton.svelte +6 -4
  87. package/dist/components/buttons/ExportButton.svelte +22 -4
  88. package/dist/components/buttons/ImportButton.svelte +83 -38
  89. package/dist/components/buttons/ImportButton.svelte.d.ts +6 -10
  90. package/dist/components/buttons/RefreshButton.svelte +48 -8
  91. package/dist/components/buttons/ShowButton.svelte +6 -4
  92. package/dist/components/content/ApiKeyList.svelte +3 -1
  93. package/dist/components/content/ContentPageShell.svelte +3 -1
  94. package/dist/components/content/FileList.svelte +3 -1
  95. package/dist/components/content/MemberList.svelte +3 -1
  96. package/dist/components/content/NetworkTable.svelte +3 -1
  97. package/dist/components/content/SecurityEventTable.svelte +3 -1
  98. package/dist/components/content/StatusTabs.svelte +2 -2
  99. package/dist/components/fieldComponentMap.d.ts +18 -13
  100. package/dist/components/fieldComponentMap.js +95 -47
  101. package/dist/components/fields/AvatarField.svelte +1 -1
  102. package/dist/components/fields/ChoiceField.svelte +16 -0
  103. package/dist/components/fields/ChoiceField.svelte.d.ts +10 -0
  104. package/dist/components/fields/CodeField.svelte +11 -12
  105. package/dist/components/fields/CodeField.svelte.d.ts +1 -1
  106. package/dist/components/fields/DateRangeField.svelte +2 -4
  107. package/dist/components/fields/ImageField.svelte +3 -1
  108. package/dist/components/fields/ImagesField.svelte +23 -0
  109. package/dist/components/fields/ImagesField.svelte.d.ts +6 -0
  110. package/dist/components/fields/JsonField.svelte +19 -13
  111. package/dist/components/fields/PasswordField.svelte +5 -0
  112. package/dist/components/fields/PasswordField.svelte.d.ts +6 -0
  113. package/dist/components/fields/json-value.d.ts +11 -0
  114. package/dist/components/fields/json-value.js +28 -0
  115. package/dist/components/profile/ActivityTimeline.svelte +2 -2
  116. package/dist/components/record-detail.svelte.d.ts +16 -0
  117. package/dist/components/record-detail.svelte.js +28 -0
  118. package/dist/components/saved-list-views.js +29 -29
  119. package/dist/components/table-contract.d.ts +13 -0
  120. package/dist/components/table-contract.js +42 -0
  121. package/dist/components/task-utils.js +4 -2
  122. package/dist/components/ui/alert/alert-variants.d.ts +2 -3
  123. package/dist/components/ui/alert/alert-variants.js +1 -2
  124. package/dist/components/ui/alert/alert.svelte +4 -5
  125. package/dist/components/ui/alert/alert.svelte.d.ts +3 -8
  126. package/dist/components/ui/alert/index.js +0 -2
  127. package/dist/components/ui/avatar/avatar-variants.d.ts +2 -3
  128. package/dist/components/ui/avatar/avatar-variants.js +1 -2
  129. package/dist/components/ui/avatar/avatar.svelte +5 -6
  130. package/dist/components/ui/avatar/avatar.svelte.d.ts +3 -8
  131. package/dist/components/ui/badge/badge-variants.d.ts +2 -3
  132. package/dist/components/ui/badge/badge-variants.js +1 -2
  133. package/dist/components/ui/badge/badge.svelte +4 -5
  134. package/dist/components/ui/badge/badge.svelte.d.ts +3 -8
  135. package/dist/components/ui/badge/index.js +0 -2
  136. package/dist/components/ui/button/button-variants.d.ts +2 -3
  137. package/dist/components/ui/button/button-variants.js +1 -2
  138. package/dist/components/ui/button/button.svelte +100 -44
  139. package/dist/components/ui/button/button.svelte.d.ts +8 -12
  140. package/dist/components/ui/button/index.js +0 -2
  141. package/dist/components/ui/command/command-dialog.svelte +3 -1
  142. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  143. package/dist/components/ui/command/command-group.svelte +4 -2
  144. package/dist/components/ui/command/command-item.svelte +4 -2
  145. package/dist/components/ui/command/command-separator.svelte +3 -1
  146. package/dist/components/ui/command/index.d.ts +5 -5
  147. package/dist/components/ui/input/input.svelte +40 -20
  148. package/dist/components/ui/input/input.svelte.d.ts +5 -4
  149. package/dist/components/ui/sheet/sheet.svelte +9 -3
  150. package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
  151. package/dist/components/ui/tooltip/tooltip.svelte +4 -2
  152. package/dist/index.d.ts +1 -1
  153. package/dist/inline-value.d.ts +5 -0
  154. package/dist/inline-value.js +24 -0
  155. package/dist/numeric-input.d.ts +2 -0
  156. package/dist/numeric-input.js +9 -0
  157. package/dist/types.d.ts +3 -3
  158. package/package.json +11 -5
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
+ import { definedOptions, definedReactiveOptions } from '@svadmin/core/options';
3
+
2
4
  import { fly } from 'svelte/transition';
3
- import { captureAdminContext, useStepsForm, deriveValidator } from '@svadmin/core';
5
+ import { tick } from 'svelte';
6
+ import { captureAdminContext, captureAuthSession, deriveValidator, useStepsForm, useResourceContract, getContractFormFields, useCan } from '@svadmin/core';
4
7
  import type { FieldDefinition } from '@svadmin/core';
5
- import { getResource } from '@svadmin/core';
6
8
  import { useTranslation } from '@svadmin/core/i18n';
7
9
 
8
10
  import { Button } from './ui/button/index.js';
@@ -11,6 +13,8 @@
11
13
  import { Progress } from './ui/progress/index.js';
12
14
  import { Save, ArrowLeft, ArrowRight, Check, Loader2 } from '@lucide/svelte';
13
15
  import FieldRenderer from './FieldRenderer.svelte';
16
+ import ConfirmDialog from './ConfirmDialog.svelte';
17
+ import { Skeleton } from './ui/skeleton/index.js';
14
18
 
15
19
  const i18n = useTranslation();
16
20
 
@@ -24,18 +28,26 @@
24
28
  id?: string | number;
25
29
  mode?: 'create' | 'edit';
26
30
  steps: StepDef[];
31
+ onSuccess?: () => void;
27
32
  }
28
33
 
29
- let { resourceName, id, mode = 'create', steps }: Props = $props();
34
+ let { resourceName, id, mode = 'create', steps, onSuccess }: Props = $props();
30
35
  const adminContext = captureAdminContext();
36
+ const binding = useResourceContract(() => resourceName);
31
37
 
32
- const resource = $derived(getResource(resourceName));
38
+ const resource = $derived(adminContext.getResource(resourceName));
33
39
  const primaryKey = $derived(resource.primaryKey ?? 'id');
40
+ const writableFields = $derived(getContractFormFields(binding.resource, mode));
41
+ const permission = useCan(() => definedOptions({ resource: resourceName, action: mode, id }));
42
+ const allowed = $derived(permission.allowed && (mode === 'create' ? resource.canCreate !== false : resource.canEdit !== false));
34
43
 
35
44
  // All fields used across all steps
36
45
  const flatStepsFields = $derived(steps.flatMap(s => s.fields)
37
46
  .map(key => resource.fields.find(f => f.key === key))
38
- .filter((f): f is FieldDefinition => f != null));
47
+ .filter((f): f is FieldDefinition => f != null && writableFields.includes(f.key)));
48
+ const configurationValid = $derived(steps.every(step => step.fields.every(key =>
49
+ flatStepsFields.some(field => field.key === key && key !== primaryKey && field.showInForm !== false &&
50
+ (mode === 'create' ? field.showInCreate !== false : field.showInEdit !== false)))));
39
51
 
40
52
  // Default values from field metadata
41
53
  function getDefaultForType(field: FieldDefinition): unknown {
@@ -57,61 +69,152 @@
57
69
  })());
58
70
 
59
71
  const validator = $derived(deriveValidator(flatStepsFields, { translate: i18n.t }));
60
- const stepsCount = $derived(steps.length);
61
72
 
62
73
  // ─── useStepsForm: form state + step navigation ──────────────────
63
- const form = useStepsForm({
64
- get resource() { return resourceName; },
74
+ const form = useStepsForm(definedReactiveOptions({
75
+ get resource() { return binding.resource; },
65
76
  get action() { return mode; },
66
77
  get id() { return id; },
67
78
  get defaultValues() { return defaults; },
68
- get stepsCount() { return stepsCount; },
79
+ get steps() { return steps.map(step => ({ fields: step.fields })); },
80
+ get enabled() { return allowed && configurationValid; },
81
+ get dataProviderName() { return binding.dataProviderName; },
69
82
  redirect: 'list',
70
83
  warnWhenUnsavedChanges: true,
71
84
  get validate() { return validator; },
72
- });
85
+ get onMutationSuccess() {
86
+ const callback = onSuccess;
87
+ return callback ? () => callback() : undefined;
88
+ },
89
+ }));
73
90
 
74
- const totalSteps = $derived(steps.length);
75
- const progressValue = $derived(((form.steps.currentStep + 1) / totalSteps) * 100);
91
+ const totalSteps = $derived(form.steps.totalSteps);
92
+ const progressValue = $derived(totalSteps > 0 ? ((form.steps.currentStep + 1) / totalSteps) * 100 : 0);
76
93
  const isLastStep = $derived(form.steps.currentStep >= totalSteps - 1);
77
94
 
78
95
  // Fields for the current step
79
96
  const currentFields = $derived(
80
97
  steps[form.steps.currentStep]?.fields
81
98
  .map(key => resource.fields.find(f => f.key === key))
82
- .filter((f): f is FieldDefinition => f != null && f.key !== primaryKey) ?? []
99
+ .filter((f): f is FieldDefinition => f != null && f.key !== primaryKey && writableFields.includes(f.key)) ?? []
83
100
  );
84
101
 
85
- // Validate only fields in the current step
86
- function validateCurrentStep(): boolean {
87
- let valid = true;
88
- form.clearErrors();
89
- const errors = validator(form.values);
90
- if (errors) {
91
- for (const field of currentFields) {
92
- if (errors[field.key]) {
93
- form.setFieldError(field.key, errors[field.key]);
94
- valid = false;
95
- }
96
- }
102
+ let element = $state.raw<HTMLFormElement>();
103
+ let submitError = $state<string | null>(null);
104
+ let submission = $state.raw<object>();
105
+ const scope = $derived({
106
+ contract: binding.resource, id, mode, allowed, configurationValid,
107
+ steps: JSON.stringify(steps), provider: adminContext.providers?.[binding.dataProviderName],
108
+ meta: binding.meta, tenant: adminContext.tenantCacheKey?.__svadminTenant,
109
+ auth: adminContext.authProvider, router: adminContext.routerProvider,
110
+ session: captureAuthSession(adminContext.authProvider),
111
+ });
112
+ const rendered = $derived({ scope, step: form.steps.currentStep });
113
+ let mounted = true;
114
+ let confirmation = $state.raw<typeof rendered>();
115
+ const currentRendered = (origin: typeof rendered) => mounted && origin === rendered &&
116
+ origin.scope.session.isCurrent() && form.ready;
117
+ const confirmOpen = $derived(confirmation !== undefined && currentRendered(confirmation));
118
+ $effect(() => {
119
+ void scope;
120
+ submitError = null;
121
+ submission = undefined;
122
+ confirmation = undefined;
123
+ });
124
+ $effect(() => () => { mounted = false; submission = undefined; confirmation = undefined; });
125
+ function errorId(field: string): string { return `${resourceName}-step-${field}-error`; }
126
+ async function focusInvalid(origin: typeof scope) {
127
+ await tick();
128
+ if (mounted && scope === origin && origin.session.isCurrent() && element?.isConnected) {
129
+ element.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus();
97
130
  }
98
- return valid;
99
131
  }
100
-
101
- async function handleSubmit() {
102
- if (!validateCurrentStep()) return;
103
-
104
- if (!isLastStep) {
105
- form.steps.nextStep();
106
- return;
107
- }
108
-
109
- // Last step — submit the full form
110
- await form.submit({ redirect: 'list' });
132
+ const changeStep = $derived.by(() => {
133
+ const origin = rendered;
134
+ return (step: number) => () => {
135
+ if (!currentRendered(origin) || !allowed || form.submitting) return;
136
+ submitError = null;
137
+ if (!form.steps.gotoStep(step)) void focusInvalid(origin.scope);
138
+ };
139
+ });
140
+ const fieldChange = $derived.by(() => {
141
+ const origin = rendered;
142
+ return (key: string) => (value: unknown) => {
143
+ if (currentRendered(origin)) form.setFieldValue(key, value);
144
+ };
145
+ });
146
+ function leave(origin: typeof rendered) {
147
+ if (!currentRendered(origin)) return;
148
+ confirmation = undefined;
149
+ void adminContext.navigate(`/${encodeURIComponent(resourceName)}`).catch(() => {
150
+ if (currentRendered(origin) && element?.isConnected) submitError = i18n.t('common.operationFailed');
151
+ });
111
152
  }
153
+ const back = $derived.by(() => {
154
+ const origin = rendered;
155
+ const previous = changeStep(origin.step - 1);
156
+ return () => {
157
+ if (!currentRendered(origin) || form.submitting) return;
158
+ if (origin.step > 0) previous();
159
+ else if (form.isDirty) confirmation = origin;
160
+ else leave(origin);
161
+ };
162
+ });
163
+ const confirmLeave = $derived.by(() => {
164
+ const origin = confirmation;
165
+ return () => { if (origin && confirmation === origin) leave(origin); };
166
+ });
167
+ const cancelLeave = $derived.by(() => {
168
+ const origin = confirmation;
169
+ return () => { if (origin && confirmation === origin) confirmation = undefined; };
170
+ });
171
+ const retry = $derived.by(() => {
172
+ const origin = scope;
173
+ const refetch = form.query.refetch;
174
+ return () => {
175
+ if (mounted && scope === origin && origin.session.isCurrent()) void refetch().catch(() => {});
176
+ };
177
+ });
178
+
179
+ const handleSubmit = $derived.by(() => {
180
+ const origin = rendered;
181
+ return async () => {
182
+ if (!currentRendered(origin) || !allowed || form.submitting || submission) return;
183
+ submitError = null;
184
+ if (!isLastStep) {
185
+ if (!form.steps.nextStep()) void focusInvalid(origin.scope);
186
+ return;
187
+ }
188
+ const token = {};
189
+ submission = token;
190
+ try { await form.submit(); }
191
+ catch {
192
+ if (submission === token && mounted && scope === origin.scope && origin.scope.session.isCurrent()) {
193
+ submitError = i18n.t('common.operationFailed');
194
+ void focusInvalid(origin.scope);
195
+ }
196
+ } finally {
197
+ if (submission === token) submission = undefined;
198
+ }
199
+ };
200
+ });
201
+ const submitEvent = $derived.by(() => {
202
+ const submit = handleSubmit;
203
+ return (event: Event) => { event.preventDefault(); void submit(); };
204
+ });
112
205
  </script>
113
206
 
114
207
  <div class="svadmin-u-b3542e058833">
208
+ {#if permission.isLoading || form.loading}
209
+ <Skeleton class="svadmin-u-426b8b75185b svadmin-u-6da6a3c3f741" />
210
+ {:else if !allowed || !configurationValid || (!form.ready && form.error)}
211
+ <p role="alert">{i18n.t('common.operationFailed')}</p>
212
+ {#if allowed && configurationValid && form.query.isError}
213
+ <Button type="button" variant="outline" onclick={retry}>
214
+ {i18n.t('common.retry')}
215
+ </Button>
216
+ {/if}
217
+ {:else if form.ready}
115
218
  <!-- Step indicators -->
116
219
  <div class="svadmin-u-6ed543e2fbbb">
117
220
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-fc7473ca09eb">
@@ -120,7 +223,9 @@
120
223
  variant="ghost"
121
224
  type="button"
122
225
  class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-ceb69a6b0e5f svadmin-u-b8f0a08ece1e svadmin-u-660d2effb880 svadmin-u-d5eab218aa34 {i === form.steps.currentStep ? 'svadmin-u-20aaf08a7ed1 svadmin-u-e83a7042bc91' : 'svadmin-u-bfa603190748'}"
123
- onclick={() => form.steps.gotoStep(i)}
226
+ aria-current={i === form.steps.currentStep ? 'step' : undefined}
227
+ disabled={form.submitting || !!submission}
228
+ onclick={changeStep(i)}
124
229
  >
125
230
  <span
126
231
  class="svadmin-u-60fbb7713999 svadmin-u-d0a52b312f7d svadmin-u-cbbf90f9a828 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-ac204c108886 svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-ceb69a6b0e5f"
@@ -154,16 +259,21 @@
154
259
  </Badge>
155
260
  </Card.CardHeader>
156
261
  <Card.CardContent>
157
- {#key form.steps.currentStep}
158
- <form class="svadmin-u-b43b4c086d9a" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} in:fly={{ x: 30, duration: 250 }}>
262
+ {#key rendered}
263
+ <form bind:this={element} class="svadmin-u-b43b4c086d9a" novalidate onsubmit={submitEvent} in:fly={{ x: 30, duration: 250 }}>
264
+ {#if submitError || form.error}
265
+ <p role="alert">{submitError ?? i18n.t('common.operationFailed')}</p>
266
+ {/if}
159
267
  {#each currentFields as field (field.key)}
160
268
  <FieldRenderer
161
269
  {field}
162
270
  value={form.values[field.key]}
163
- onchange={(v) => form.setFieldValue(field.key, v)}
271
+ onchange={fieldChange(field.key)}
272
+ invalid={!!form.errors[field.key]}
273
+ {...definedOptions({ errorId: form.errors[field.key] ? errorId(field.key) : undefined })}
164
274
  />
165
275
  {#if form.errors[field.key]}
166
- <p class="svadmin-u-811148b13d1e svadmin-u-1d5904e7e755 svadmin-u-b6b02c0ebef6">{form.errors[field.key]}</p>
276
+ <p id={errorId(field.key)} role="alert" aria-live="polite" class="svadmin-u-811148b13d1e svadmin-u-1d5904e7e755 svadmin-u-b6b02c0ebef6">{form.errors[field.key]}</p>
167
277
  {/if}
168
278
  {/each}
169
279
 
@@ -171,13 +281,14 @@
171
281
  <Button
172
282
  type="button"
173
283
  variant="outline"
174
- onclick={() => form.steps.currentStep === 0 ? adminContext.navigate(`/${resourceName}`) : form.steps.prevStep()}
284
+ disabled={form.submitting || !!submission}
285
+ onclick={back}
175
286
  >
176
287
  <ArrowLeft class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" />
177
288
  {form.steps.currentStep === 0 ? i18n.t('common.cancel') : i18n.t('common.back')}
178
289
  </Button>
179
290
 
180
- <Button type="submit" disabled={form.submitting}>
291
+ <Button type="submit" disabled={form.submitting || !!submission}>
181
292
  {#if form.submitting}
182
293
  <Loader2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-afbdd13a380e" data-icon="inline-start" />
183
294
  {:else if isLastStep}
@@ -192,4 +303,10 @@
192
303
  {/key}
193
304
  </Card.CardContent>
194
305
  </Card.Root>
306
+ {/if}
195
307
  </div>
308
+
309
+ {#key confirmation}
310
+ <ConfirmDialog open={confirmOpen} message={i18n.t('common.unsavedChanges')}
311
+ onconfirm={confirmLeave} oncancel={cancelLeave} />
312
+ {/key}
@@ -7,6 +7,7 @@ interface Props {
7
7
  id?: string | number;
8
8
  mode?: 'create' | 'edit';
9
9
  steps: StepDef[];
10
+ onSuccess?: () => void;
10
11
  }
11
12
  declare const StepsForm: import("svelte").Component<Props, {}, "">;
12
13
  type StepsForm = ReturnType<typeof StepsForm>;
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { useTask, getTaskProvider } from '@svadmin/core';
5
+ import { decodeTaskRecord } from '@svadmin/core/schema';
3
6
  import { useTranslation } from '@svadmin/core/i18n';
4
7
 
5
8
  import type { TaskProvider, TaskRecord } from '@svadmin/core';
@@ -27,7 +30,7 @@
27
30
  let {
28
31
  task,
29
32
  taskId,
30
- taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined,
33
+ taskProvider = getTaskProvider({ optional: true }) ?? undefined,
31
34
  title,
32
35
  useProviderData = !task && !!taskId,
33
36
  queryOptions,
@@ -59,13 +62,21 @@
59
62
  },
60
63
  });
61
64
 
62
- const resolvedTask = $derived(task ?? query.data);
65
+ const view = $derived.by(() => {
66
+ const value = task === undefined ? query.data : task;
67
+ try {
68
+ return { data: value === undefined ? undefined : decodeTaskRecord(value, taskId), invalid: false };
69
+ } catch {
70
+ return { data: undefined, invalid: true };
71
+ }
72
+ });
73
+ const resolvedTask = $derived(view.data);
63
74
  const resolvedTitle = $derived(title ?? i18n.t('task.detailsTitle'));
64
75
 
65
76
  function formatDate(value: unknown) {
66
- if (!value) return '—';
67
- const date = new Date(value as string | Date);
68
- if (Number.isNaN(date.getTime())) return String(value);
77
+ if (typeof value !== 'string' || !value) return '—';
78
+ const date = new Date(value);
79
+ if (Number.isNaN(date.getTime())) return '—';
69
80
  return date.toLocaleString();
70
81
  }
71
82
 
@@ -90,7 +101,9 @@
90
101
  </Card.Description>
91
102
  </Card.Header>
92
103
  <Card.Content class="svadmin-u-b43b4c086d9a svadmin-u-9335c39f6eff">
93
- {#if useProviderData && taskProvider && query.isLoading}
104
+ {#if view.invalid || (useProviderData && query.isError)}
105
+ <p role="alert">{i18n.t('validation.invalidFormat')}</p>
106
+ {:else if useProviderData && taskProvider && query.isLoading}
94
107
  <div class="svadmin-u-60fbb7713999 svadmin-u-aadad6871af8 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-bfa603190748">
95
108
  <Loader2 class="svadmin-u-d2347e8497a9 svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-afbdd13a380e" />
96
109
  {i18n.t('task.loadingDetail')}
@@ -102,7 +115,7 @@
102
115
  </div>
103
116
 
104
117
  {#if typeof resolveTaskProgress(resolvedTask) === 'number'}
105
- <TaskProgressBar value={resolveTaskProgress(resolvedTask)} />
118
+ <TaskProgressBar {...definedOptions({ "value": resolveTaskProgress(resolvedTask) })} />
106
119
  {/if}
107
120
 
108
121
  <div class="svadmin-u-f3c543ad5fe9 svadmin-u-0c3bc98565dd svadmin-u-e4d6f343b9ff">
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { useTaskList, getTaskProvider } from '@svadmin/core';
5
+ import { decodeTaskList } from '@svadmin/core/schema';
3
6
  import { useTranslation } from '@svadmin/core/i18n';
4
7
 
5
8
  import type { TaskProvider, TaskRecord } from '@svadmin/core';
@@ -24,7 +27,7 @@
24
27
 
25
28
  let {
26
29
  tasks,
27
- taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined,
30
+ taskProvider = getTaskProvider({ optional: true }) ?? undefined,
28
31
  params,
29
32
  dlq = false,
30
33
  title,
@@ -70,14 +73,21 @@
70
73
  },
71
74
  });
72
75
 
73
- const resolvedTasks = $derived(tasks ?? query.data?.data ?? []);
76
+ const view = $derived.by(() => {
77
+ try {
78
+ return { data: decodeTaskList({ data: tasks === undefined ? query.data?.data ?? [] : tasks }).data, invalid: false };
79
+ } catch {
80
+ return { data: [], invalid: true };
81
+ }
82
+ });
83
+ const resolvedTasks = $derived(view.data);
74
84
  const resolvedTitle = $derived(title ?? (dlq ? i18n.t('task.dlqTitle') : i18n.t('task.listTitle')));
75
85
  const resolvedEmptyText = $derived(emptyText ?? (dlq ? i18n.t('task.noDlq') : i18n.t('task.noTasks')));
76
86
 
77
87
  function formatDate(value: unknown) {
78
- if (!value) return '—';
79
- const date = new Date(value as string | Date);
80
- if (Number.isNaN(date.getTime())) return String(value);
88
+ if (typeof value !== 'string' || !value) return '—';
89
+ const date = new Date(value);
90
+ if (Number.isNaN(date.getTime())) return '—';
81
91
  return date.toLocaleString();
82
92
  }
83
93
  </script>
@@ -94,7 +104,9 @@
94
104
  </Card.Description>
95
105
  </Card.Header>
96
106
  <Card.Content class="svadmin-u-9335c39f6eff">
97
- {#if useProviderData && taskProvider && query.isLoading}
107
+ {#if view.invalid || (useProviderData && query.isError)}
108
+ <p role="alert">{i18n.t('validation.invalidFormat')}</p>
109
+ {:else if useProviderData && taskProvider && query.isLoading}
98
110
  <div class="svadmin-u-60fbb7713999 svadmin-u-aadad6871af8 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-bfa603190748">
99
111
  <Loader2 class="svadmin-u-d2347e8497a9 svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-afbdd13a380e" />
100
112
  {i18n.t('task.loadingList')}
@@ -122,7 +134,7 @@
122
134
  <div class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">{resolveTaskMessage(task)}</div>
123
135
  {/if}
124
136
  {#if showProgress && typeof resolveTaskProgress(task) === 'number'}
125
- <TaskProgressBar value={resolveTaskProgress(task)} />
137
+ <TaskProgressBar {...definedOptions({ "value": resolveTaskProgress(task) })} />
126
138
  {/if}
127
139
  </div>
128
140
  </Table.Cell>
@@ -1,6 +1,9 @@
1
1
  <script lang="ts">
2
+ import { definedReactiveOptions, definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { ListTodo, Loader2, Plus, Search, RefreshCw } from '@lucide/svelte';
3
5
  import { getTaskProvider, useSubmitTask, useTaskList } from '@svadmin/core';
6
+ import { decodeTaskSubmitOptions } from '@svadmin/core/schema';
4
7
  import { useTranslation } from '@svadmin/core/i18n';
5
8
  import type { TaskProvider, TaskRecord } from '@svadmin/core';
6
9
  import { Button } from './ui/button/index.js';
@@ -28,7 +31,7 @@
28
31
 
29
32
  let {
30
33
  open = $bindable(false),
31
- taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined,
34
+ taskProvider = getTaskProvider({ optional: true }) ?? undefined,
32
35
  title,
33
36
  initialTab = 'tasks',
34
37
  }: {
@@ -38,8 +41,7 @@
38
41
  initialTab?: TaskTab;
39
42
  } = $props();
40
43
 
41
- // eslint-disable-next-line svelte/prefer-writable-derived
42
- let activeTab = $state<TaskTab>('tasks');
44
+ let activeTab = $derived(initialTab);
43
45
  let selectedTaskId = $state<string | null>(null);
44
46
  let searchQuery = $state('');
45
47
  let statusFilter = $state('all');
@@ -55,7 +57,7 @@
55
57
  );
56
58
  const resolvedTitle = $derived(title ?? i18n.t('task.drawerTitle'));
57
59
 
58
- const taskQuery = useTaskList({
60
+ const taskQuery = useTaskList(definedReactiveOptions({
59
61
  get taskProvider() {
60
62
  return taskProvider;
61
63
  },
@@ -66,9 +68,9 @@
66
68
  refetchIntervalInBackground: true,
67
69
  };
68
70
  },
69
- });
71
+ }));
70
72
 
71
- const dlqQuery = useTaskList({
73
+ const dlqQuery = useTaskList(definedReactiveOptions({
72
74
  get dlq() {
73
75
  return true;
74
76
  },
@@ -82,7 +84,7 @@
82
84
  refetchIntervalInBackground: true,
83
85
  };
84
86
  },
85
- });
87
+ }));
86
88
 
87
89
  const submitTask = useSubmitTask();
88
90
 
@@ -123,10 +125,6 @@
123
125
  allTasks.filter(isTaskActive).length,
124
126
  );
125
127
 
126
- $effect(() => {
127
- activeTab = initialTab;
128
- });
129
-
130
128
  $effect(() => {
131
129
  if (statusFilter !== 'all' && !availableStatuses.includes(statusFilter)) {
132
130
  statusFilter = 'all';
@@ -141,19 +139,19 @@
141
139
  }
142
140
 
143
141
  if (!selectedTaskId) {
144
- selectedTaskId = current[0].id;
142
+ selectedTaskId = current[0]?.id ?? null;
145
143
  return;
146
144
  }
147
145
 
148
146
  if (!current.some((task) => task.id === selectedTaskId)) {
149
- selectedTaskId = current[0].id;
147
+ selectedTaskId = current[0]?.id ?? null;
150
148
  }
151
149
  });
152
150
 
153
151
  function formatTime(value: unknown) {
154
- if (!value) return '—';
155
- const date = new Date(value as string | Date);
156
- if (Number.isNaN(date.getTime())) return String(value);
152
+ if (typeof value !== 'string' || !value) return '—';
153
+ const date = new Date(value);
154
+ if (Number.isNaN(date.getTime())) return '—';
157
155
  return date.toLocaleString();
158
156
  }
159
157
 
@@ -174,7 +172,8 @@
174
172
  let body: Record<string, unknown> | undefined;
175
173
  if (submitBodyText.trim()) {
176
174
  try {
177
- body = JSON.parse(submitBodyText) as Record<string, unknown>;
175
+ const parsed: unknown = JSON.parse(submitBodyText);
176
+ body = decodeTaskSubmitOptions({ body: parsed }).body;
178
177
  } catch {
179
178
  submitError = i18n.t('task.invalidJson');
180
179
  return;
@@ -182,16 +181,16 @@
182
181
  }
183
182
 
184
183
  try {
185
- const handle = await submitTask.mutation.mutateAsync({
184
+ const handle = await submitTask.mutation.mutateAsync(definedOptions({
186
185
  taskName: submitTaskName.trim(),
187
186
  taskProvider,
188
- options: {
187
+ options: definedOptions({
189
188
  body,
190
189
  idempotencyKey: submitIdempotencyKey.trim() || undefined,
191
- },
192
- });
190
+ }),
191
+ }));
193
192
  activeTab = 'tasks';
194
- selectedTaskId = handle.id ?? null;
193
+ selectedTaskId = handle.id;
195
194
  submitOpen = false;
196
195
  resetSubmitForm();
197
196
  await Promise.all([
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { onMount } from 'svelte';
3
5
  import { Toaster, toast as sonner, type ToasterProps } from 'svelte-sonner';
4
6
  import {
@@ -29,7 +31,7 @@
29
31
  const queue = getToastQueue();
30
32
  if (queue.length > 0) {
31
33
  for (const t of queue) {
32
- const options = { duration: t.duration, id: t.key ?? t.id };
34
+ const options = definedOptions({ duration: t.duration, id: t.key ?? t.id });
33
35
  switch (t.type) {
34
36
  case 'success': sonner.success(t.message, options); break;
35
37
  case 'error': sonner.error(t.message, options); break;
@@ -86,7 +88,7 @@
86
88
  {#each getToasts().slice(-3) as item (item.id)}
87
89
  <UndoableNotification
88
90
  message={item.message}
89
- duration={item.duration}
91
+ {...definedOptions({ "duration": item.duration })}
90
92
  embedded
91
93
  managedExternally
92
94
  onUndo={() => undo(item.id, item.onUndo)}
@@ -60,7 +60,7 @@
60
60
  <Tooltip.Root bind:open>
61
61
  <Tooltip.Trigger aria-describedby={restProps['aria-describedby']}>
62
62
  {#snippet child({ props })}
63
- {@const primitiveOnclick = props.onclick as ((event: MouseEvent) => void) | undefined}
63
+ {@const primitiveOnclick = props['onclick'] as ((event: MouseEvent) => void) | undefined}
64
64
  {@const buttonProps = { ...mergeButtonProps(props), href, target, rel, variant, size, type, class: className, 'aria-label': restProps['aria-label'] ?? tooltip, 'aria-describedby': props['aria-describedby'], onclick: (event: MouseEvent) => handleClick(event, primitiveOnclick), disabled, tabindex } as any}
65
65
  <Button {...buttonProps}>
66
66
  {@render children()}
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { SvelteSet } from 'svelte/reactivity';
3
5
  import { ChevronRight, ChevronDown, Check, X, Search, ChevronsUpDown } from '@lucide/svelte';
4
6
  import { cn } from '../utils.js';
@@ -140,10 +142,10 @@
140
142
  const matchChildren = filteredChildren && filteredChildren.length > 0;
141
143
 
142
144
  if (matchSelf || matchChildren) {
143
- filtered.push({
145
+ filtered.push(definedOptions({
144
146
  ...node,
145
147
  children: filteredChildren ?? node.children,
146
- });
148
+ }));
147
149
  }
148
150
  }
149
151
  return filtered;
@@ -0,0 +1,27 @@
1
+ <script lang="ts" generics="P extends Record<string, unknown>">
2
+ import type { Component } from 'svelte';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import type { FieldDisplayProps } from './fieldComponentMap';
5
+
6
+ let { component: Display, resolve, input }: {
7
+ component: Component<P>;
8
+ resolve: (input: FieldDisplayProps) => { ok: true; props: P } | { ok: false };
9
+ input: FieldDisplayProps;
10
+ } = $props();
11
+
12
+ const i18n = useTranslation();
13
+ const result = $derived.by(() => {
14
+ try {
15
+ return resolve(input);
16
+ } catch {
17
+ // Invalid recursive values and schema failures must not reach the renderer.
18
+ return { ok: false } as const;
19
+ }
20
+ });
21
+ </script>
22
+
23
+ {#if result.ok}
24
+ <Display {...result.props} />
25
+ {:else}
26
+ <span role="status" data-svadmin-invalid-field>{i18n.t('validation.invalidFormat')}</span>
27
+ {/if}