@svadmin/ui 0.72.0 → 0.73.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 (252) hide show
  1. package/README.md +40 -25
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/dist/app.css +7694 -5117
  4. package/dist/app.theme.css +7645 -5119
  5. package/dist/class-properties.generated.d.ts +1 -0
  6. package/dist/class-properties.generated.js +2 -0
  7. package/dist/classnames.d.ts +8 -0
  8. package/dist/classnames.js +26 -0
  9. package/dist/components/AutoForm.svelte +20 -6
  10. package/dist/components/AutoForm.svelte.d.ts +2 -0
  11. package/dist/components/AutoTable.svelte +249 -9
  12. package/dist/components/AutoTable.svelte.d.ts +5 -0
  13. package/dist/components/BoundRecordDetailDrawer.svelte +8 -2
  14. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
  15. package/dist/components/ComboboxField.svelte +31 -9
  16. package/dist/components/ComboboxField.svelte.d.ts +3 -2
  17. package/dist/components/CreatePage.svelte +4 -0
  18. package/dist/components/CreatePage.svelte.d.ts +2 -0
  19. package/dist/components/DateRangeInput.svelte +75 -0
  20. package/dist/components/DateRangeInput.svelte.d.ts +25 -0
  21. package/dist/components/DateTimeInput.svelte +65 -0
  22. package/dist/components/DateTimeInput.svelte.d.ts +19 -0
  23. package/dist/components/DevTools.svelte +2 -2
  24. package/dist/components/EditPage.svelte +4 -0
  25. package/dist/components/EditPage.svelte.d.ts +2 -0
  26. package/dist/components/FieldRenderer.svelte +90 -18
  27. package/dist/components/FileUpload.svelte +250 -0
  28. package/dist/components/FileUpload.svelte.d.ts +32 -0
  29. package/dist/components/FilterBuilder.svelte +236 -279
  30. package/dist/components/FilterBuilder.svelte.d.ts +3 -0
  31. package/dist/components/ImportWizard.svelte +181 -94
  32. package/dist/components/JsonSchemaForm.svelte +255 -99
  33. package/dist/components/JsonSchemaForm.svelte.d.ts +12 -1
  34. package/dist/components/Layout.svelte +1 -2
  35. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
  36. package/dist/components/ListPage.svelte +4 -0
  37. package/dist/components/ListPage.svelte.d.ts +2 -0
  38. package/dist/components/LoginPage.svelte +1 -1
  39. package/dist/components/MoneyInput.svelte +25 -0
  40. package/dist/components/MoneyInput.svelte.d.ts +20 -0
  41. package/dist/components/NumberInput.svelte +25 -0
  42. package/dist/components/NumberInput.svelte.d.ts +19 -0
  43. package/dist/components/NumericInput.svelte +110 -0
  44. package/dist/components/NumericInput.svelte.d.ts +23 -0
  45. package/dist/components/PercentInput.svelte +25 -0
  46. package/dist/components/PercentInput.svelte.d.ts +20 -0
  47. package/dist/components/QuickEditDrawer.svelte +4 -1
  48. package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
  49. package/dist/components/RecordDetailDrawer.svelte +4 -2
  50. package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
  51. package/dist/components/ResourceOperationsPage.svelte +25 -19
  52. package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
  53. package/dist/components/ShowInferencer.svelte +4 -2
  54. package/dist/components/ShowInferencer.svelte.d.ts +2 -0
  55. package/dist/components/ShowPage.svelte +7 -3
  56. package/dist/components/ShowPage.svelte.d.ts +2 -0
  57. package/dist/components/Sidebar.svelte +5 -5
  58. package/dist/components/SpreadsheetView.svelte +81 -286
  59. package/dist/components/StepsForm.svelte +5 -5
  60. package/dist/components/SvarAutoTable.svelte +39 -0
  61. package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
  62. package/dist/components/SvarDataGrid.svelte +362 -0
  63. package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
  64. package/dist/components/SvarGridCell.svelte +10 -0
  65. package/dist/components/SvarGridCell.svelte.d.ts +7 -0
  66. package/dist/components/SvarResourceRead.svelte +50 -0
  67. package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
  68. package/dist/components/SvarResourceTable.svelte +63 -0
  69. package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
  70. package/dist/components/SvarResourceTableView.svelte +364 -0
  71. package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
  72. package/dist/components/TaskQueueDrawer.svelte +72 -16
  73. package/dist/components/TreeSelect.svelte +208 -28
  74. package/dist/components/TreeSelect.svelte.d.ts +3 -1
  75. package/dist/components/auto-table-grid.d.ts +26 -0
  76. package/dist/components/auto-table-grid.js +1 -0
  77. package/dist/components/content/ContentPageHeader.svelte +12 -9
  78. package/dist/components/content/ContentPageShell.svelte +6 -9
  79. package/dist/components/content/FileBrowser.svelte +35 -0
  80. package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
  81. package/dist/components/content/MetricBlock.svelte +10 -13
  82. package/dist/components/content/NetworkTable.svelte +3 -3
  83. package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
  84. package/dist/components/content/PageToolbar.svelte +6 -10
  85. package/dist/components/content/SectionHeader.svelte +8 -13
  86. package/dist/components/content/SettingsFieldRow.svelte +9 -23
  87. package/dist/components/content/SettingsGroup.svelte +11 -27
  88. package/dist/components/content/StatusBadge.svelte +3 -8
  89. package/dist/components/content/WorkspaceLayout.svelte +8 -12
  90. package/dist/components/enterprise/filter-tree.d.ts +48 -0
  91. package/dist/components/enterprise/filter-tree.js +224 -0
  92. package/dist/components/enterprise/json-schema-form.d.ts +39 -0
  93. package/dist/components/enterprise/json-schema-form.js +195 -0
  94. package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
  95. package/dist/components/enterprise/recursive-schema-form.js +196 -0
  96. package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
  97. package/dist/components/enterprise/spreadsheet-formula.js +245 -0
  98. package/dist/components/fieldComponentMap.js +5 -0
  99. package/dist/components/fields/DateField.svelte +4 -3
  100. package/dist/components/fields/date-field-value.d.ts +2 -0
  101. package/dist/components/fields/date-field-value.js +13 -0
  102. package/dist/components/json-schema-form-state.d.ts +37 -0
  103. package/dist/components/json-schema-form-state.js +168 -0
  104. package/dist/components/record-detail.svelte.d.ts +3 -0
  105. package/dist/components/record-detail.svelte.js +10 -4
  106. package/dist/components/svar-grid-cells.d.ts +14 -0
  107. package/dist/components/svar-grid-cells.js +2 -0
  108. package/dist/components/svar-grid-contract.d.ts +80 -0
  109. package/dist/components/svar-grid-contract.js +159 -0
  110. package/dist/components/svar-grid-interactions.d.ts +54 -0
  111. package/dist/components/svar-grid-interactions.js +174 -0
  112. package/dist/components/svar-grid-loading.d.ts +40 -0
  113. package/dist/components/svar-grid-loading.js +180 -0
  114. package/dist/components/svar-grid-operations.d.ts +37 -0
  115. package/dist/components/svar-grid-operations.js +186 -0
  116. package/dist/components/svar-grid-panes.d.ts +34 -0
  117. package/dist/components/svar-grid-panes.js +66 -0
  118. package/dist/components/svar-grid-resource-loading.d.ts +18 -0
  119. package/dist/components/svar-grid-resource-loading.js +68 -0
  120. package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
  121. package/dist/components/svar-grid-resource-session.svelte.js +61 -0
  122. package/dist/components/ui/badge/badge-variants.js +3 -1
  123. package/dist/components/ui/button/button-variants.js +5 -2
  124. package/dist/components/ui/input/input.svelte +1 -4
  125. package/dist/components/ui/textarea/textarea.svelte +2 -1
  126. package/dist/components.css +3079 -0
  127. package/dist/design-contract.d.ts +13 -0
  128. package/dist/design-contract.js +11 -0
  129. package/dist/design-system.d.ts +15 -0
  130. package/dist/design-system.js +7 -0
  131. package/dist/index.d.ts +14 -0
  132. package/dist/index.js +9 -0
  133. package/dist/page-chrome.css +12 -49
  134. package/dist/recipes.d.ts +4 -0
  135. package/dist/recipes.js +4 -0
  136. package/dist/rendering/index.d.ts +87 -0
  137. package/dist/rendering/index.js +117 -0
  138. package/dist/styled-system/css/conditions.js +36 -0
  139. package/dist/styled-system/css/css.d.ts +22 -0
  140. package/dist/styled-system/css/css.js +45 -0
  141. package/dist/styled-system/css/cva.d.ts +6 -0
  142. package/dist/styled-system/css/cva.js +87 -0
  143. package/dist/styled-system/css/cx.d.ts +5 -0
  144. package/dist/styled-system/css/cx.js +15 -0
  145. package/dist/styled-system/css/index.d.ts +5 -0
  146. package/dist/styled-system/css/index.js +4 -0
  147. package/dist/styled-system/css/sva.d.ts +4 -0
  148. package/dist/styled-system/css/sva.js +46 -0
  149. package/dist/styled-system/helpers.js +316 -0
  150. package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
  151. package/dist/styled-system/patterns/aspect-ratio.js +38 -0
  152. package/dist/styled-system/patterns/bleed.d.ts +21 -0
  153. package/dist/styled-system/patterns/bleed.js +24 -0
  154. package/dist/styled-system/patterns/box.d.ts +20 -0
  155. package/dist/styled-system/patterns/box.js +15 -0
  156. package/dist/styled-system/patterns/center.d.ts +20 -0
  157. package/dist/styled-system/patterns/center.js +21 -0
  158. package/dist/styled-system/patterns/circle.d.ts +20 -0
  159. package/dist/styled-system/patterns/circle.js +25 -0
  160. package/dist/styled-system/patterns/container.d.ts +20 -0
  161. package/dist/styled-system/patterns/container.js +21 -0
  162. package/dist/styled-system/patterns/cq.d.ts +21 -0
  163. package/dist/styled-system/patterns/cq.js +21 -0
  164. package/dist/styled-system/patterns/divider.d.ts +22 -0
  165. package/dist/styled-system/patterns/divider.js +25 -0
  166. package/dist/styled-system/patterns/flex.d.ts +26 -0
  167. package/dist/styled-system/patterns/flex.js +26 -0
  168. package/dist/styled-system/patterns/float.d.ts +23 -0
  169. package/dist/styled-system/patterns/float.js +52 -0
  170. package/dist/styled-system/patterns/grid-item.d.ts +25 -0
  171. package/dist/styled-system/patterns/grid-item.js +25 -0
  172. package/dist/styled-system/patterns/grid.d.ts +24 -0
  173. package/dist/styled-system/patterns/grid.js +27 -0
  174. package/dist/styled-system/patterns/hstack.d.ts +21 -0
  175. package/dist/styled-system/patterns/hstack.js +24 -0
  176. package/dist/styled-system/patterns/index.d.ts +21 -0
  177. package/dist/styled-system/patterns/index.js +20 -0
  178. package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
  179. package/dist/styled-system/patterns/link-overlay.js +24 -0
  180. package/dist/styled-system/patterns/spacer.d.ts +20 -0
  181. package/dist/styled-system/patterns/spacer.js +25 -0
  182. package/dist/styled-system/patterns/square.d.ts +20 -0
  183. package/dist/styled-system/patterns/square.js +24 -0
  184. package/dist/styled-system/patterns/stack.d.ts +23 -0
  185. package/dist/styled-system/patterns/stack.js +24 -0
  186. package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
  187. package/dist/styled-system/patterns/visually-hidden.js +18 -0
  188. package/dist/styled-system/patterns/vstack.d.ts +21 -0
  189. package/dist/styled-system/patterns/vstack.js +24 -0
  190. package/dist/styled-system/patterns/wrap.d.ts +24 -0
  191. package/dist/styled-system/patterns/wrap.js +25 -0
  192. package/dist/styled-system/recipes/content-header.d.ts +31 -0
  193. package/dist/styled-system/recipes/content-header.js +65 -0
  194. package/dist/styled-system/recipes/content-page.d.ts +34 -0
  195. package/dist/styled-system/recipes/content-page.js +43 -0
  196. package/dist/styled-system/recipes/create-recipe.js +82 -0
  197. package/dist/styled-system/recipes/index.d.ts +17 -0
  198. package/dist/styled-system/recipes/index.js +16 -0
  199. package/dist/styled-system/recipes/metric-block.d.ts +34 -0
  200. package/dist/styled-system/recipes/metric-block.js +76 -0
  201. package/dist/styled-system/recipes/product-list.d.ts +31 -0
  202. package/dist/styled-system/recipes/product-list.js +73 -0
  203. package/dist/styled-system/recipes/product-section.d.ts +31 -0
  204. package/dist/styled-system/recipes/product-section.js +49 -0
  205. package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
  206. package/dist/styled-system/recipes/product-settings-row.js +58 -0
  207. package/dist/styled-system/recipes/product-settings.d.ts +31 -0
  208. package/dist/styled-system/recipes/product-settings.js +57 -0
  209. package/dist/styled-system/recipes/product-status.d.ts +34 -0
  210. package/dist/styled-system/recipes/product-status.js +45 -0
  211. package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
  212. package/dist/styled-system/recipes/product-toolbar.js +41 -0
  213. package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
  214. package/dist/styled-system/recipes/product-workspace.js +58 -0
  215. package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
  216. package/dist/styled-system/recipes/surface-metric.js +63 -0
  217. package/dist/styled-system/recipes/surface-table.d.ts +34 -0
  218. package/dist/styled-system/recipes/surface-table.js +62 -0
  219. package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
  220. package/dist/styled-system/recipes/ui-badge.js +38 -0
  221. package/dist/styled-system/recipes/ui-button.d.ts +32 -0
  222. package/dist/styled-system/recipes/ui-button.js +43 -0
  223. package/dist/styled-system/recipes/ui-input.d.ts +31 -0
  224. package/dist/styled-system/recipes/ui-input.js +49 -0
  225. package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
  226. package/dist/styled-system/recipes/ui-textarea.js +24 -0
  227. package/dist/styled-system/styles.css +1257 -0
  228. package/dist/styled-system/tokens/index.d.ts +9 -0
  229. package/dist/styled-system/tokens/index.js +268 -0
  230. package/dist/styled-system/tokens/tokens.d.ts +36 -0
  231. package/dist/styled-system/types/composition.d.ts +227 -0
  232. package/dist/styled-system/types/conditions.d.ts +236 -0
  233. package/dist/styled-system/types/csstype.d.ts +22570 -0
  234. package/dist/styled-system/types/index.d.ts +6 -0
  235. package/dist/styled-system/types/parts.d.ts +8 -0
  236. package/dist/styled-system/types/pattern.d.ts +78 -0
  237. package/dist/styled-system/types/prop-type.d.ts +223 -0
  238. package/dist/styled-system/types/recipe.d.ts +181 -0
  239. package/dist/styled-system/types/selectors.d.ts +59 -0
  240. package/dist/styled-system/types/static-css.d.ts +56 -0
  241. package/dist/styled-system/types/style-props.d.ts +8088 -0
  242. package/dist/styled-system/types/system-types.d.ts +151 -0
  243. package/dist/styles/aliases.css +49 -0
  244. package/dist/styles/compatibility.epilogue.css +384 -0
  245. package/dist/styles/compatibility.prelude.css +5513 -0
  246. package/dist/styles/conditional-states.css +28 -0
  247. package/dist/styles/conditional-utilities.css +22 -0
  248. package/dist/styles/recipes.css +1262 -0
  249. package/dist/utils.d.ts +1 -1
  250. package/dist/utils.js +1 -1
  251. package/package.json +31 -13
  252. package/styles-compatibility.json +12 -0
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import { getResource, captureAdminContext } from '@svadmin/core';
2
+ import { captureAdminContext, captureAuthSession, useCan, useImport, useResourceContract } from '@svadmin/core';
3
+ import { untrack } from 'svelte';
4
+ import { definedReactiveOptions } from '@svadmin/core/options';
3
5
  import { useTranslation } from '@svadmin/core/i18n';
4
6
  import { parseCSV, toCsv } from '@svadmin/core';
5
7
  import * as Dialog from './ui/dialog/index.js';
@@ -23,65 +25,118 @@
23
25
  }: Props = $props();
24
26
 
25
27
  const i18n = useTranslation();
26
- const adminContext = captureAdminContext();
27
- const resource = $derived(getResource(resourceName));
28
+ const context = captureAdminContext();
29
+ const binding = useResourceContract(() => resourceName);
30
+ const resource = $derived(context.getResource(resourceName));
31
+ const can = useCan(() => ({ resource: resourceName, action: 'import' }));
32
+ const session = $derived(captureAuthSession(context.authProvider));
33
+ const allowed = $derived(can.allowed === true && resource.canCreate !== false && session.available);
34
+ const scope = $derived({
35
+ contract: binding.resource, provider: context.providers?.[binding.dataProviderName],
36
+ tenant: context.tenantCacheKey?.__svadminTenant, meta: binding.meta,
37
+ auth: context.authProvider, router: context.routerProvider, session, allowed,
38
+ });
28
39
  const availableFields = $derived(
29
40
  resource.fields.filter((f) => f.key !== (resource.primaryKey ?? 'id') && f.showInForm !== false)
30
41
  );
31
42
 
32
43
  let currentStep = $state<1 | 2 | 3>(1);
33
44
  let fileName = $state('');
45
+ let selectedFile = $state<File | null>(null);
34
46
  let rawHeaders = $state<string[]>([]);
35
- let rawRows = $state<string[][]>([]);
47
+ let rawRows = $state<unknown[][]>([]);
36
48
  let columnMapping = $state<Record<string, string>>({}); // header -> fieldKey or ''
49
+ let fileError = $state<string | null>(null);
50
+ let importError = $state<string | null>(null);
51
+ let parseToken = 0;
52
+ let mounted = true;
37
53
 
38
- let isImporting = $state(false);
39
- let processedCount = $state(0);
54
+ let importResult = $state<{ succeeded: number; failed: number } | null>(null);
40
55
  let succeededCount = $state(0);
41
56
  let failedRecords = $state<Array<{ row: Record<string, unknown>; error: string }>>([]);
42
57
 
58
+ const importer = useImport(definedReactiveOptions({
59
+ get resource() { return binding.resource; },
60
+ get dataProviderName() { return binding.dataProviderName; },
61
+ get batchSize() { return batchSize; },
62
+ get enabled() { return open && allowed; },
63
+ mapData: mapRow,
64
+ }));
65
+ const isImporting = $derived(importer.isLoading);
66
+ const processedCount = $derived(importer.progress.processedAmount);
67
+ const totalCount = $derived(importer.progress.totalAmount || rawRows.length);
43
68
  const progressPercent = $derived(
44
- rawRows.length > 0 ? Math.min(100, Math.round((processedCount / rawRows.length) * 100)) : 0
69
+ totalCount > 0 ? Math.min(100, Math.round((processedCount / totalCount) * 100)) : 0
45
70
  );
46
71
 
47
- async function handleFileSelect(selectedFile: File) {
48
- fileName = selectedFile.name;
49
- const text = await selectedFile.text();
50
- const cleanText = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
72
+ async function handleFileSelect(file: File) {
73
+ if (!open || !allowed || isImporting) return;
74
+ const origin = scope;
75
+ const token = ++parseToken;
76
+ fileError = null;
77
+ importError = null;
78
+ selectedFile = null;
79
+ fileName = file.name;
80
+ rawHeaders = [];
81
+ rawRows = [];
82
+ columnMapping = {};
83
+ try {
84
+ const text = await file.text();
85
+ if (!current(origin, token)) return;
86
+ const cleanText = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
51
87
 
52
- if (fileName.endsWith('.json')) {
53
- try {
54
- const json = JSON.parse(cleanText);
55
- if (Array.isArray(json) && json.length > 0) {
56
- const keys = Object.keys(json[0]);
57
- rawHeaders = keys;
58
- rawRows = json.map((item) => keys.map((k) => String(item[k] ?? '')));
88
+ if (file.name.toLowerCase().endsWith('.json')) {
89
+ const json: unknown = JSON.parse(cleanText);
90
+ if (!Array.isArray(json) || json.length === 0 || json.some((item) => (
91
+ typeof item !== 'object' || item === null || Array.isArray(item)
92
+ ))) {
93
+ throw new Error('The JSON file must contain a non-empty array of records.');
94
+ }
95
+ const records = json as Record<string, unknown>[];
96
+ const keys = [...new Set(records.flatMap(item => Object.keys(item)))];
97
+ if (keys.length === 0) throw new Error('The import file has no columns.');
98
+ rawHeaders = keys;
99
+ rawRows = records.map((item) => keys.map((key) => item[key]));
100
+ } else if (file.name.toLowerCase().endsWith('.csv')) {
101
+ const rows = parseCSV(cleanText);
102
+ const firstRow = rows[0];
103
+ if (!firstRow || firstRow.length === 0 || firstRow.every((cell) => !cell.trim())) {
104
+ throw new Error('The CSV file has no header row.');
105
+ }
106
+ if (firstRow.some(header => !header.trim()) || new Set(firstRow).size !== firstRow.length) {
107
+ throw new Error('CSV column names must be non-empty and unique.');
108
+ }
109
+ const records = rows.slice(1).filter((r: string[]) =>
110
+ r.some((cell: string) => cell.trim().length > 0));
111
+ if (records.length === 0) throw new Error('The import file has no records.');
112
+ if (records.some((row) => row.length !== firstRow.length)) {
113
+ throw new Error('The CSV file contains rows with inconsistent column counts.');
59
114
  }
60
- } catch {
61
- /* invalid json */
62
- }
63
- } else {
64
- const rows = parseCSV(cleanText);
65
- const firstRow = rows[0];
66
- if (firstRow) {
67
115
  rawHeaders = firstRow;
68
- rawRows = rows.slice(1).filter((r: string[]) => r.some((cell: string) => cell.trim().length > 0));
116
+ rawRows = records;
117
+ } else {
118
+ throw new Error('Only CSV and JSON files are supported.');
69
119
  }
70
- }
71
120
 
72
- // Auto-match headers to fields by key or label
73
- const initialMapping: Record<string, string> = {};
74
- for (const h of rawHeaders) {
75
- const normalized = h.toLowerCase().trim().replace(/[-_]/g, '');
76
- const match = availableFields.find(
77
- (f) =>
78
- f.key.toLowerCase().replace(/[-_]/g, '') === normalized ||
79
- f.label.toLowerCase().replace(/[-_]/g, '') === normalized
80
- );
81
- initialMapping[h] = match ? match.key : '';
121
+ if (!current(origin, token)) return;
122
+ selectedFile = file;
123
+ const initialMapping: Record<string, string> = {};
124
+ for (const h of rawHeaders) {
125
+ const normalized = h.toLowerCase().trim().replace(/[-_]/g, '');
126
+ const match = availableFields.find(
127
+ (f) =>
128
+ f.key.toLowerCase().replace(/[-_]/g, '') === normalized ||
129
+ f.label.toLowerCase().replace(/[-_]/g, '') === normalized
130
+ );
131
+ initialMapping[h] = match ? match.key : '';
132
+ }
133
+ columnMapping = initialMapping;
134
+ currentStep = 2;
135
+ } catch (error) {
136
+ if (!current(origin, token)) return;
137
+ fileError = error instanceof Error ? error.message : 'Unable to read the import file.';
138
+ currentStep = 1;
82
139
  }
83
- columnMapping = initialMapping;
84
- currentStep = 2;
85
140
  }
86
141
 
87
142
  function handleDrop(e: DragEvent) {
@@ -91,63 +146,67 @@
91
146
  }
92
147
  }
93
148
 
149
+ function mapRow(row: Record<string, unknown>): Record<string, unknown> {
150
+ const record: Record<string, unknown> = {};
151
+ rawHeaders.forEach((header) => {
152
+ const targetFieldKey = columnMapping[header];
153
+ if (!targetFieldKey) return;
154
+ const fieldDef = availableFields.find((f) => f.key === targetFieldKey);
155
+ let val: unknown = row[header];
156
+ if (typeof val === 'string') {
157
+ if (fieldDef?.type === 'number' || fieldDef?.type === 'currency' || fieldDef?.type === 'percent') {
158
+ const num = val.trim() === '' ? NaN : Number(val);
159
+ val = val.trim() === '' ? undefined : Number.isFinite(num) ? num : val;
160
+ } else if (fieldDef?.type === 'boolean') {
161
+ const normalized = val.trim().toLowerCase();
162
+ if (['true', '1', 'yes', '是'].includes(normalized)) val = true;
163
+ else if (['false', '0', 'no', '否'].includes(normalized)) val = false;
164
+ }
165
+ }
166
+ if (val !== undefined) {
167
+ Object.defineProperty(record, targetFieldKey, { value: val, enumerable: true });
168
+ }
169
+ });
170
+ return record;
171
+ }
172
+
94
173
  async function startImport() {
174
+ if (!selectedFile || rawRows.length === 0 || isImporting || !open || !allowed) return;
175
+ const targets = Object.values(columnMapping).filter(Boolean);
176
+ if (targets.length === 0 || new Set(targets).size !== targets.length) {
177
+ fileError = 'Map at least one column, and use each target field only once.';
178
+ return;
179
+ }
180
+ const origin = scope;
181
+ const token = parseToken;
182
+ const notifySuccess = onSuccess;
95
183
  currentStep = 3;
96
- isImporting = true;
97
- processedCount = 0;
98
184
  succeededCount = 0;
99
185
  failedRecords = [];
100
-
101
- const provider = adminContext.getDataProviderForResource(resourceName);
102
-
103
- // Build mapped records
104
- const recordsToImport: Record<string, unknown>[] = [];
105
- for (const row of rawRows) {
106
- const record: Record<string, unknown> = {};
107
- rawHeaders.forEach((header, idx) => {
108
- const targetFieldKey = columnMapping[header];
109
- if (targetFieldKey) {
110
- const fieldDef = availableFields.find((f) => f.key === targetFieldKey);
111
- let val: unknown = row[idx] ?? '';
112
- if (fieldDef?.type === 'number') {
113
- const num = Number(val);
114
- val = isNaN(num) ? val : num;
115
- } else if (fieldDef?.type === 'boolean') {
116
- val = val === 'true' || val === '1' || val === 'yes' || val === '是';
117
- }
118
- record[targetFieldKey] = val;
119
- }
120
- });
121
- recordsToImport.push(record);
186
+ importResult = null;
187
+ importError = null;
188
+ try {
189
+ const result = await importer.handleChange({ file: selectedFile });
190
+ if (!current(origin, token)) return;
191
+ succeededCount = result.succeeded.length;
192
+ failedRecords = result.errored.map(item => ({
193
+ row: item.request as Record<string, unknown>,
194
+ error: item.error.message,
195
+ }));
196
+ importResult = { succeeded: succeededCount, failed: failedRecords.length };
197
+ } catch (error) {
198
+ if (!current(origin, token)) return;
199
+ importError = error instanceof Error ? error.message : 'Import failed.';
200
+ importResult = null;
122
201
  }
123
-
124
- // Execute in batches
125
- for (let i = 0; i < recordsToImport.length; i += batchSize) {
126
- const batch = recordsToImport.slice(i, i + batchSize);
127
- if (provider.createMany) {
128
- try {
129
- await provider.createMany({ resource: resourceName, variables: batch });
130
- succeededCount += batch.length;
131
- } catch (err) {
132
- batch.forEach((item) => {
133
- failedRecords.push({ row: item, error: err instanceof Error ? err.message : String(err) });
134
- });
135
- }
136
- } else {
137
- for (const item of batch) {
138
- try {
139
- await provider.create({ resource: resourceName, variables: item });
140
- succeededCount++;
141
- } catch (err) {
142
- failedRecords.push({ row: item, error: err instanceof Error ? err.message : String(err) });
143
- }
144
- }
145
- }
146
- processedCount = Math.min(recordsToImport.length, i + batch.length);
202
+ if (importResult) {
203
+ try { notifySuccess?.(importResult); }
204
+ catch { /* 消费方回调失败不能把已完成的写入改判为导入失败。 */ }
147
205
  }
206
+ }
148
207
 
149
- isImporting = false;
150
- onSuccess?.({ succeeded: succeededCount, failed: failedRecords.length });
208
+ function current(origin: typeof scope, token: number): boolean {
209
+ return mounted && open && allowed && origin === scope && origin.session.isCurrent() && token === parseToken;
151
210
  }
152
211
 
153
212
  function downloadErrors() {
@@ -167,16 +226,27 @@
167
226
  }
168
227
 
169
228
  function reset() {
229
+ parseToken += 1;
170
230
  currentStep = 1;
171
231
  fileName = '';
232
+ selectedFile = null;
172
233
  rawHeaders = [];
173
234
  rawRows = [];
174
235
  columnMapping = {};
175
- processedCount = 0;
176
236
  succeededCount = 0;
177
237
  failedRecords = [];
178
- isImporting = false;
238
+ importResult = null;
239
+ fileError = null;
240
+ importError = null;
241
+ importer.reset();
179
242
  }
243
+
244
+ $effect.pre(() => {
245
+ void scope;
246
+ void open;
247
+ untrack(reset);
248
+ });
249
+ $effect(() => () => { mounted = false; parseToken += 1; });
180
250
  </script>
181
251
 
182
252
  {#if open}
@@ -199,6 +269,9 @@
199
269
  </div>
200
270
 
201
271
  <!-- Step 1: Upload -->
272
+ {#if !allowed}
273
+ <p role="status">{i18n.t('common.operationFailed', { defaultValue: 'Import is unavailable.' })}</p>
274
+ {/if}
202
275
  {#if currentStep === 1}
203
276
  <div
204
277
  class="svadmin-u-65935df577ba svadmin-u-a29b7a649c77 svadmin-u-c9ed8c5f79ae svadmin-u-8b9d5d768973 svadmin-u-a217b4eaa918 svadmin-u-845f53365c8d svadmin-u-ca6bf63030aa svadmin-u-ceb69a6b0e5f svadmin-u-34516836730d svadmin-u-967d113a1451"
@@ -218,6 +291,7 @@
218
291
  <input
219
292
  type="file"
220
293
  accept=".csv,.json"
294
+ disabled={!allowed}
221
295
  class="svadmin-u-99d72c7fc3e2"
222
296
  onchange={(e) => {
223
297
  const target = e.currentTarget;
@@ -228,12 +302,16 @@
228
302
  {i18n.t('common.selectFile', { defaultValue: 'Browse File' })}
229
303
  </span>
230
304
  </label>
305
+ {#if fileError}
306
+ <p role="alert">{fileError}</p>
307
+ {/if}
231
308
  </div>
232
309
  {/if}
233
310
 
234
311
  <!-- Step 2: Column Mapping -->
235
312
  {#if currentStep === 2}
236
313
  <div class="svadmin-u-3e7ce58d64fa">
314
+ {#if fileError}<p role="alert">{fileError}</p>{/if}
237
315
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-2859c861d7de svadmin-u-9fe52d5d506c svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-05faf5c801ff">
238
316
  <span>{i18n.t('common.detectedRows', { defaultValue: 'Detected' })}: <strong class="svadmin-u-d4108abe6359">{rawRows.length}</strong> {i18n.t('common.records', { defaultValue: 'records' })}</span>
239
317
  <Badge variant="secondary" class="svadmin-u-d058ca6de60f svadmin-u-0e65706bcccd">{fileName}</Badge>
@@ -252,8 +330,13 @@
252
330
  </div>
253
331
  <div class="svadmin-u-74b2435a1d40 svadmin-u-012fbd121f37">
254
332
  <select
333
+ aria-label={`Map ${header}`}
255
334
  class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
256
- bind:value={columnMapping[header]}
335
+ value={columnMapping[header]}
336
+ onchange={(event) => {
337
+ columnMapping = { ...columnMapping, [header]: event.currentTarget.value };
338
+ fileError = null;
339
+ }}
257
340
  >
258
341
  <option value="">— {i18n.t('common.ignoreColumn', { defaultValue: 'Ignore' })} —</option>
259
342
  {#each availableFields as field (field.key)}
@@ -272,7 +355,7 @@
272
355
  <ArrowLeft class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-618162408e7a" />
273
356
  {i18n.t('common.back', { defaultValue: 'Back' })}
274
357
  </Button>
275
- <Button size="sm" onclick={startImport}>
358
+ <Button size="sm" disabled={!allowed || isImporting} onclick={startImport}>
276
359
  {i18n.t('common.startImport', { defaultValue: 'Start Import' })}
277
360
  <ArrowRight class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-f58b02572ab2" />
278
361
  </Button>
@@ -285,7 +368,7 @@
285
368
  <div class="svadmin-u-b43b4c086d9a svadmin-u-03b4dd7f172b">
286
369
  <div class="svadmin-u-6f7e013d6499">
287
370
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-359090c2d529">
288
- <span class="svadmin-u-bfa603190748">{isImporting ? i18n.t('common.importing', { defaultValue: 'Importing...' }) : i18n.t('common.completed', { defaultValue: 'Completed' })}</span>
371
+ <span class="svadmin-u-bfa603190748">{isImporting ? i18n.t('common.importing', { defaultValue: 'Importing...' }) : importError ? i18n.t('common.operationFailed', { defaultValue: 'Import failed' }) : i18n.t('common.completed', { defaultValue: 'Completed' })}</span>
289
372
  <span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359 svadmin-u-3032cae0badb">{progressPercent}%</span>
290
373
  </div>
291
374
  <Progress value={progressPercent} class="svadmin-u-2f2a842e50fa" />
@@ -302,6 +385,10 @@
302
385
  </div>
303
386
  </div>
304
387
 
388
+ {#if importError}
389
+ <p role="alert">{importError}</p>
390
+ {/if}
391
+
305
392
  {#if failedRecords.length > 0}
306
393
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-eb6e8b881acd svadmin-u-5f22e64f2282 svadmin-u-7a0854fdbc30 svadmin-u-ca6bcd4b6f3f svadmin-u-e09a917c4c3b svadmin-u-359090c2d529">
307
394
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-811148b13d1e">