@svadmin/ui 0.88.1 → 0.92.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 (172) hide show
  1. package/dist/app.css +4 -0
  2. package/dist/app.theme.css +4 -0
  3. package/dist/component-registry.svelte.d.ts +2 -2
  4. package/dist/components/AboutSettings.svelte +23 -4
  5. package/dist/components/AdminApp.svelte +6 -4
  6. package/dist/components/AdminApp.svelte.d.ts +3 -3
  7. package/dist/components/ApiSettings.svelte +43 -27
  8. package/dist/components/ApprovalActionCard.svelte +8 -9
  9. package/dist/components/ApprovalCenter.svelte +24 -5
  10. package/dist/components/AuditLogViewer.svelte +2 -2
  11. package/dist/components/AutoComplete.svelte +610 -0
  12. package/dist/components/AutoComplete.svelte.d.ts +25 -0
  13. package/dist/components/AutoForm.svelte +265 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +141 -36
  16. package/dist/components/AutoTable.svelte.d.ts +8 -0
  17. package/dist/components/BatchActionBar.svelte +42 -14
  18. package/dist/components/BatchActionBar.svelte.d.ts +5 -0
  19. package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
  20. package/dist/components/CancelTaskButton.svelte +14 -1
  21. package/dist/components/Cascader.svelte +412 -53
  22. package/dist/components/Cascader.svelte.d.ts +7 -1
  23. package/dist/components/ColorPicker.svelte +1 -1
  24. package/dist/components/ColorPicker.svelte.d.ts +1 -1
  25. package/dist/components/ComboboxField.svelte +270 -34
  26. package/dist/components/ComboboxField.svelte.d.ts +4 -0
  27. package/dist/components/CommandPalette.svelte +4 -4
  28. package/dist/components/DashboardView.svelte +73 -7
  29. package/dist/components/DashboardView.svelte.d.ts +3 -1
  30. package/dist/components/DateRangeInput.svelte +8 -1
  31. package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
  32. package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
  33. package/dist/components/DetailWorkspaceSections.svelte +233 -0
  34. package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
  35. package/dist/components/DrawerForm.svelte +78 -9
  36. package/dist/components/DrawerForm.svelte.d.ts +3 -3
  37. package/dist/components/ErrorBoundary.svelte +1 -1
  38. package/dist/components/ErrorComponent.svelte +19 -6
  39. package/dist/components/ErrorComponent.svelte.d.ts +1 -0
  40. package/dist/components/ErrorSummary.svelte +9 -4
  41. package/dist/components/FieldRenderer.svelte +203 -90
  42. package/dist/components/FieldRenderer.svelte.d.ts +16 -1
  43. package/dist/components/FileUpload.svelte +116 -51
  44. package/dist/components/FileUpload.svelte.d.ts +7 -27
  45. package/dist/components/FileUploadField.svelte +170 -0
  46. package/dist/components/FileUploadField.svelte.d.ts +21 -0
  47. package/dist/components/FilterBuilder.svelte +13 -7
  48. package/dist/components/FilterCollectionInput.svelte +9 -7
  49. package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
  50. package/dist/components/ForgotPasswordPage.svelte +1 -1
  51. package/dist/components/Header.svelte +1 -1
  52. package/dist/components/ImportWizard.svelte +197 -28
  53. package/dist/components/ImportWizard.svelte.d.ts +5 -1
  54. package/dist/components/InferencerPanel.svelte +4 -2
  55. package/dist/components/InfiniteList.svelte +52 -9
  56. package/dist/components/JsonSchemaForm.svelte +237 -42
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
  58. package/dist/components/KanbanBoard.svelte +43 -7
  59. package/dist/components/KanbanBoard.svelte.d.ts +2 -2
  60. package/dist/components/Layout.svelte +1 -1
  61. package/dist/components/LoginPage.svelte +1 -1
  62. package/dist/components/MetricStrip.svelte +8 -3
  63. package/dist/components/MetricStrip.svelte.d.ts +1 -0
  64. package/dist/components/ModalForm.svelte +85 -9
  65. package/dist/components/ModalForm.svelte.d.ts +3 -3
  66. package/dist/components/MoneyInput.svelte +5 -3
  67. package/dist/components/MoneyInput.svelte.d.ts +4 -2
  68. package/dist/components/MultiTabKeepAlive.svelte +502 -43
  69. package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
  70. package/dist/components/NotificationsSettings.svelte +10 -10
  71. package/dist/components/NumberInput.svelte +5 -3
  72. package/dist/components/NumberInput.svelte.d.ts +4 -2
  73. package/dist/components/NumericInput.svelte +277 -25
  74. package/dist/components/NumericInput.svelte.d.ts +3 -1
  75. package/dist/components/PercentInput.svelte +5 -3
  76. package/dist/components/PercentInput.svelte.d.ts +4 -2
  77. package/dist/components/PermissionMatrix.svelte +44 -8
  78. package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
  79. package/dist/components/ProfilePage.svelte +38 -13
  80. package/dist/components/RadioGroup.svelte +203 -4
  81. package/dist/components/RadioGroup.svelte.d.ts +1 -1
  82. package/dist/components/Rate.svelte +3 -3
  83. package/dist/components/Rate.svelte.d.ts +2 -2
  84. package/dist/components/RecordDetailDrawer.svelte +1 -1
  85. package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
  86. package/dist/components/RegisterPage.svelte +1 -1
  87. package/dist/components/RelationPicker.svelte +39 -11
  88. package/dist/components/ResourceAccessGuard.svelte +7 -2
  89. package/dist/components/RetryTaskButton.svelte +14 -1
  90. package/dist/components/RolesSettings.svelte +74 -28
  91. package/dist/components/SecuritySettings.svelte +66 -51
  92. package/dist/components/SegmentedControl.svelte +4 -2
  93. package/dist/components/SegmentedControl.svelte.d.ts +1 -1
  94. package/dist/components/Sidebar.svelte +1 -1
  95. package/dist/components/StepsForm.svelte +62 -10
  96. package/dist/components/StepsForm.svelte.d.ts +4 -2
  97. package/dist/components/SvadminLogo.svelte +1 -0
  98. package/dist/components/SvarDataGrid.svelte +42 -24
  99. package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
  100. package/dist/components/SvarResourceRead.svelte +2 -0
  101. package/dist/components/SvarResourceTableView.svelte +4 -2
  102. package/dist/components/TagsInput.svelte +3 -3
  103. package/dist/components/TagsInput.svelte.d.ts +2 -2
  104. package/dist/components/TaskDetails.svelte +22 -16
  105. package/dist/components/TaskDetails.svelte.d.ts +1 -1
  106. package/dist/components/TaskList.svelte +57 -15
  107. package/dist/components/TaskQueueDrawer.svelte +54 -34
  108. package/dist/components/Toast.svelte +13 -2
  109. package/dist/components/Transfer.svelte +67 -5
  110. package/dist/components/Transfer.svelte.d.ts +4 -0
  111. package/dist/components/TreeSelect.svelte +337 -77
  112. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  113. package/dist/components/TreeTable.svelte +6 -1
  114. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  115. package/dist/components/UpdatePasswordPage.svelte +1 -1
  116. package/dist/components/VersionDiffViewer.svelte +9 -7
  117. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  118. package/dist/components/account/MemberDirectory.svelte +14 -10
  119. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  120. package/dist/components/content/AuditTimeline.svelte +8 -6
  121. package/dist/components/content/DataState.svelte +1 -1
  122. package/dist/components/content/FileList.svelte +44 -13
  123. package/dist/components/content/FileList.svelte.d.ts +4 -2
  124. package/dist/components/content/FileListRow.svelte +87 -0
  125. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  126. package/dist/components/content/ImportDropzone.svelte +109 -12
  127. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  128. package/dist/components/content/IntegrationCard.svelte +3 -3
  129. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  130. package/dist/components/content/MediaThumbnail.svelte +6 -2
  131. package/dist/components/detail-workspace.d.ts +60 -0
  132. package/dist/components/detail-workspace.js +3 -0
  133. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  134. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  135. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  136. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  137. package/dist/components/enterprise/schema-form-references.js +316 -0
  138. package/dist/components/fields/ArrayField.svelte +178 -14
  139. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  140. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  141. package/dist/components/fields/array-field-metadata.js +36 -0
  142. package/dist/components/file-upload-reference.d.ts +11 -0
  143. package/dist/components/file-upload-reference.js +50 -0
  144. package/dist/components/file-upload-types.d.ts +32 -0
  145. package/dist/components/file-upload-types.js +1 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  147. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  148. package/dist/components/json-schema-form-state.d.ts +19 -0
  149. package/dist/components/list-deletion.svelte.js +6 -1
  150. package/dist/components/list-state.svelte.d.ts +12 -1
  151. package/dist/components/list-state.svelte.js +56 -16
  152. package/dist/components/numeric-format.d.ts +44 -0
  153. package/dist/components/numeric-format.js +198 -0
  154. package/dist/components/record-detail.svelte.js +2 -0
  155. package/dist/components/saved-list-views.d.ts +9 -2
  156. package/dist/components/saved-list-views.js +42 -11
  157. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  158. package/dist/components/saved-list-views.svelte.js +56 -7
  159. package/dist/components/task-action.svelte.d.ts +1 -0
  160. package/dist/components/task-action.svelte.js +55 -3
  161. package/dist/components/task-utils.d.ts +1 -0
  162. package/dist/components/task-utils.js +18 -0
  163. package/dist/components/ui/input/input.svelte +1 -1
  164. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  165. package/dist/components/ui/sheet/sheet.svelte +4 -1
  166. package/dist/components/version-history.d.ts +14 -0
  167. package/dist/components/version-history.js +32 -0
  168. package/dist/components/workspace-tab-state.d.ts +78 -0
  169. package/dist/components/workspace-tab-state.js +587 -0
  170. package/dist/index.d.ts +13 -1
  171. package/dist/index.js +4 -0
  172. package/package.json +5 -5
@@ -0,0 +1,316 @@
1
+ import { SCHEMA_FORM_LIMITS } from '../json-schema-form-state.js';
2
+ const unsafe = new Set(['__proto__', 'prototype', 'constructor']);
3
+ const internalRef = /^#\/\$defs\/([^/]+)$/u;
4
+ const externalRef = /^([^#]+)(?:#(.*))?$/u;
5
+ const MAX_REFERENCES = 256;
6
+ const MAX_DEFINITIONS = 256;
7
+ const typeBoxSymbols = new Set([
8
+ Symbol.for('TypeBox.Kind'), Symbol.for('TypeBox.Optional'), Symbol.for('TypeBox.Readonly'),
9
+ ]);
10
+ const typeBoxMetadata = new Set(['~kind', '~optional', '~readonly']);
11
+ const pointer = (key) => '/' + key.replaceAll('~', '~0').replaceAll('/', '~1');
12
+ /** 只接受由数据属性组成的普通对象,不执行 getter。 */
13
+ function dataRecord(value, schemaNode = false) {
14
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
15
+ return false;
16
+ const prototype = Object.getPrototypeOf(value);
17
+ if (prototype !== Object.prototype && prototype !== null)
18
+ return false;
19
+ return Reflect.ownKeys(value).every(key => {
20
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
21
+ if (typeof key === 'symbol') {
22
+ return schemaNode && typeBoxSymbols.has(key)
23
+ && descriptor !== undefined && 'value' in descriptor
24
+ && typeof descriptor.value === 'string';
25
+ }
26
+ if (schemaNode && descriptor?.enumerable === false) {
27
+ return typeBoxMetadata.has(key) && 'value' in descriptor;
28
+ }
29
+ return descriptor !== undefined && 'value' in descriptor;
30
+ });
31
+ }
32
+ function own(record, key) {
33
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
34
+ return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;
35
+ }
36
+ function decodePointerSegment(segment) {
37
+ if (!segment)
38
+ return undefined;
39
+ let result = '';
40
+ for (let index = 0; index < segment.length; index++) {
41
+ const character = segment[index];
42
+ if (character !== '~') {
43
+ result += character;
44
+ continue;
45
+ }
46
+ const escape = segment[++index];
47
+ if (escape === '0')
48
+ result += '~';
49
+ else if (escape === '1')
50
+ result += '/';
51
+ else
52
+ return undefined;
53
+ }
54
+ return result;
55
+ }
56
+ function validDocumentUri(uri) {
57
+ if (uri.length === 0 || uri.includes('#'))
58
+ return false;
59
+ for (const character of uri) {
60
+ const code = character.codePointAt(0) ?? 0;
61
+ if (code <= 0x20 || code === 0x7f)
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+ function referenceTarget(reference, localDefinitions, documents) {
67
+ const local = internalRef.exec(reference);
68
+ if (local) {
69
+ const name = decodePointerSegment(local[1] ?? '');
70
+ if (name === undefined || unsafe.has(name) || !localDefinitions || !Object.hasOwn(localDefinitions, name))
71
+ return undefined;
72
+ return { target: own(localDefinitions, name), definitions: localDefinitions, root: false };
73
+ }
74
+ const match = externalRef.exec(reference);
75
+ if (!match)
76
+ return undefined;
77
+ const uri = match[1] ?? '';
78
+ if (!validDocumentUri(uri) || !Object.hasOwn(documents, uri))
79
+ return undefined;
80
+ const document = own(documents, uri);
81
+ if (!dataRecord(document, true))
82
+ return undefined;
83
+ const fragment = match[2];
84
+ if (fragment !== undefined && fragment !== '' && !fragment.startsWith('/$defs/'))
85
+ return undefined;
86
+ let definitions;
87
+ const candidates = own(document, '$defs');
88
+ if (candidates !== undefined) {
89
+ if (!dataRecord(candidates) || Object.keys(candidates).length > MAX_DEFINITIONS)
90
+ return undefined;
91
+ definitions = candidates;
92
+ }
93
+ if (fragment === undefined || fragment === '')
94
+ return { target: document, definitions, root: true };
95
+ const name = decodePointerSegment(fragment.slice('/$defs/'.length));
96
+ if (name === undefined || unsafe.has(name) || !definitions || !Object.hasOwn(definitions, name))
97
+ return undefined;
98
+ return { target: own(definitions, name), definitions, root: false };
99
+ }
100
+ /**
101
+ * 解析受控的本地外部引用子集。
102
+ * 外部引用必须命中宿主传入的精确 URI 映射,只允许根文档或 `/$defs/<name>`;
103
+ * 不执行网络、fetch/XHR、动态 import、storage 或环境读取。
104
+ */
105
+ export function resolveSchemaFormReferences(schema, referenceDocuments = {}) {
106
+ const issues = [];
107
+ const active = new Set();
108
+ const activeData = new Set();
109
+ let localDefinitions;
110
+ let expansions = 0;
111
+ let nodes = 0;
112
+ const issue = (path, code) => {
113
+ if (issues.length < 128)
114
+ issues.push({ path, code });
115
+ };
116
+ if (!dataRecord(referenceDocuments)) {
117
+ issue('', 'ref-invalid');
118
+ referenceDocuments = {};
119
+ }
120
+ function copyData(raw, path, depth) {
121
+ if (++nodes > SCHEMA_FORM_LIMITS.nodes || depth > SCHEMA_FORM_LIMITS.depth) {
122
+ issue(path, 'invalid-schema');
123
+ return undefined;
124
+ }
125
+ if (raw === undefined || raw === null || typeof raw === 'boolean' || typeof raw === 'string')
126
+ return raw;
127
+ if (typeof raw === 'number') {
128
+ if (Number.isFinite(raw))
129
+ return raw;
130
+ issue(path, 'invalid-schema');
131
+ return undefined;
132
+ }
133
+ if (typeof raw !== 'object' || activeData.has(raw)) {
134
+ issue(path, 'invalid-schema');
135
+ return undefined;
136
+ }
137
+ activeData.add(raw);
138
+ try {
139
+ if (Array.isArray(raw)) {
140
+ if (Reflect.ownKeys(raw).some(key => typeof key === 'symbol') || raw.length > SCHEMA_FORM_LIMITS.nodes) {
141
+ issue(path, 'invalid-schema');
142
+ return undefined;
143
+ }
144
+ for (let index = 0; index < raw.length; index++) {
145
+ const descriptor = Object.getOwnPropertyDescriptor(raw, String(index));
146
+ if (!descriptor || !('value' in descriptor)) {
147
+ issue(path + '/' + index, 'invalid-schema');
148
+ return undefined;
149
+ }
150
+ }
151
+ if (Object.keys(raw).some(key => !/^(0|[1-9]\d*)$/u.test(key) || Number(key) >= raw.length)) {
152
+ issue(path, 'invalid-schema');
153
+ return undefined;
154
+ }
155
+ const result = [];
156
+ for (let index = 0; index < raw.length; index++) {
157
+ result.push(copyData(own(raw, String(index)), path + '/' + index, depth + 1));
158
+ }
159
+ return result;
160
+ }
161
+ if (!dataRecord(raw)) {
162
+ issue(path, 'invalid-schema');
163
+ return undefined;
164
+ }
165
+ const result = {};
166
+ for (const key of Object.keys(raw)) {
167
+ if (unsafe.has(key)) {
168
+ issue(path + pointer(key), 'invalid-schema');
169
+ continue;
170
+ }
171
+ const source = own(raw, key);
172
+ const value = copyData(source, path + pointer(key), depth + 1);
173
+ if (value !== undefined || source === undefined) {
174
+ Object.defineProperty(result, key, { value, enumerable: true, configurable: true, writable: true });
175
+ }
176
+ }
177
+ return result;
178
+ }
179
+ finally {
180
+ activeData.delete(raw);
181
+ }
182
+ }
183
+ function collectDefinitions(raw, path) {
184
+ const candidates = own(raw, '$defs');
185
+ if (candidates === undefined)
186
+ return undefined;
187
+ if (!dataRecord(candidates) || Object.keys(candidates).length > MAX_DEFINITIONS) {
188
+ issue(path + '/$defs', 'invalid-schema');
189
+ return undefined;
190
+ }
191
+ const collected = Object.create(null);
192
+ for (const key of Object.keys(candidates)) {
193
+ if (unsafe.has(key)) {
194
+ issue(path + '/$defs' + pointer(key), 'invalid-schema');
195
+ continue;
196
+ }
197
+ collected[key] = own(candidates, key);
198
+ }
199
+ return collected;
200
+ }
201
+ function expand(raw, path, depth, refDepth, root, definitions = localDefinitions) {
202
+ if (++nodes > SCHEMA_FORM_LIMITS.nodes || depth > SCHEMA_FORM_LIMITS.depth) {
203
+ issue(path, 'invalid-schema');
204
+ return undefined;
205
+ }
206
+ if (!dataRecord(raw, true) || active.has(raw)) {
207
+ issue(path, 'invalid-schema');
208
+ return undefined;
209
+ }
210
+ active.add(raw);
211
+ try {
212
+ if (Object.hasOwn(raw, '$ref')) {
213
+ const schemaKeys = Object.getOwnPropertyNames(raw).filter(key => !(['~kind', '~optional', '~readonly'].includes(key)
214
+ && Object.getOwnPropertyDescriptor(raw, key)?.enumerable === false));
215
+ if (schemaKeys.length !== 1) {
216
+ issue(path, 'unsupported-schema');
217
+ return undefined;
218
+ }
219
+ const reference = own(raw, '$ref');
220
+ if (typeof reference !== 'string') {
221
+ issue(path, 'ref-invalid');
222
+ return undefined;
223
+ }
224
+ const resolved = referenceTarget(reference, definitions, referenceDocuments);
225
+ if (!resolved) {
226
+ issue(path, 'ref-invalid');
227
+ return undefined;
228
+ }
229
+ if (++expansions > MAX_REFERENCES || refDepth + 1 > SCHEMA_FORM_LIMITS.depth) {
230
+ issue(path, 'ref-limit');
231
+ return undefined;
232
+ }
233
+ const target = resolved.target;
234
+ if (target !== null && typeof target === 'object' && active.has(target)) {
235
+ issue(path, 'ref-cycle');
236
+ return undefined;
237
+ }
238
+ return expand(target, path, depth, refDepth + 1, resolved.root, resolved.definitions);
239
+ }
240
+ const currentDefinitions = root ? collectDefinitions(raw, path) : definitions;
241
+ if (root)
242
+ localDefinitions = currentDefinitions;
243
+ const result = {};
244
+ for (const key of Object.keys(raw)) {
245
+ if (root && key === '$defs')
246
+ continue;
247
+ if (key === 'properties') {
248
+ const properties = own(raw, key);
249
+ if (!dataRecord(properties) || Object.keys(properties).length > MAX_DEFINITIONS) {
250
+ issue(path + '/properties', 'invalid-schema');
251
+ continue;
252
+ }
253
+ const expanded = {};
254
+ for (const name of Object.keys(properties)) {
255
+ if (unsafe.has(name)) {
256
+ issue(path + '/properties' + pointer(name), 'invalid-schema');
257
+ continue;
258
+ }
259
+ const child = expand(own(properties, name), path + pointer(name), depth + 1, 0, false, currentDefinitions);
260
+ if (child !== undefined)
261
+ Object.defineProperty(expanded, name, { value: child, enumerable: true, configurable: true, writable: true });
262
+ }
263
+ Object.defineProperty(result, key, { value: expanded, enumerable: true, configurable: true, writable: true });
264
+ continue;
265
+ }
266
+ if (key === 'items') {
267
+ const item = expand(own(raw, key), path + '/items', depth + 1, 0, false, currentDefinitions);
268
+ if (item !== undefined)
269
+ Object.defineProperty(result, key, { value: item, enumerable: true, configurable: true, writable: true });
270
+ continue;
271
+ }
272
+ if (key === 'oneOf') {
273
+ const branches = own(raw, key);
274
+ if (!Array.isArray(branches) || branches.length === 0 || branches.length > MAX_DEFINITIONS
275
+ || Reflect.ownKeys(branches).some(branchKey => typeof branchKey === 'symbol')
276
+ || Object.keys(branches).some(branchKey => !/^(0|[1-9]\d*)$/u.test(branchKey) || Number(branchKey) >= branches.length)) {
277
+ issue(path + '/oneOf', 'oneOf-invalid');
278
+ continue;
279
+ }
280
+ const expanded = [];
281
+ for (let index = 0; index < branches.length; index++) {
282
+ const descriptor = Object.getOwnPropertyDescriptor(branches, String(index));
283
+ if (!descriptor || !('value' in descriptor)) {
284
+ issue(path + '/oneOf/' + index, 'oneOf-invalid');
285
+ continue;
286
+ }
287
+ const branch = expand(descriptor.value, path + '/oneOf/' + index, depth + 1, 0, false, currentDefinitions);
288
+ if (branch !== undefined)
289
+ expanded.push(branch);
290
+ }
291
+ if (expanded.length === branches.length)
292
+ Object.defineProperty(result, key, { value: expanded, enumerable: true, configurable: true, writable: true });
293
+ continue;
294
+ }
295
+ const source = own(raw, key);
296
+ const value = copyData(source, path + pointer(key), 0);
297
+ if (value !== undefined || source === undefined)
298
+ Object.defineProperty(result, key, { value, enumerable: true, configurable: true, writable: true });
299
+ }
300
+ return result;
301
+ }
302
+ finally {
303
+ active.delete(raw);
304
+ }
305
+ }
306
+ let resolved;
307
+ try {
308
+ const next = expand(schema, '', 0, 0, true);
309
+ if (next !== undefined && dataRecord(next))
310
+ resolved = next;
311
+ }
312
+ catch {
313
+ issue('', 'invalid-schema');
314
+ }
315
+ return { schema: issues.length ? undefined : resolved, issues };
316
+ }
@@ -2,20 +2,95 @@
2
2
  import type { FieldDefinition } from '@svadmin/core';
3
3
  import { useTranslation } from '@svadmin/core/i18n';
4
4
  import { Button } from '../ui/button/index.js';
5
- import { Plus, Trash2 } from '@lucide/svelte';
5
+ import { ArrowDown, ArrowUp, Plus, Trash2 } from '@lucide/svelte';
6
6
  import TooltipButton from '../TooltipButton.svelte';
7
7
  import FieldRenderer from '../FieldRenderer.svelte';
8
+ import type { UploadCancelHandler, UploadHandler } from '../file-upload-types.js';
9
+ import { arrayFieldRowKey as editedRowKey, arrayFieldOrderKey as outputOrderKey } from './array-field-metadata.js';
8
10
 
9
11
  const i18n = useTranslation();
10
12
 
11
- let { field, value, onchange } = $props<{
13
+ interface Props {
12
14
  field: FieldDefinition;
13
15
  value: unknown;
14
16
  onchange: (val: unknown) => void;
15
- }>();
17
+ disabled?: boolean;
18
+ labelledby?: string;
19
+ describedby?: string;
20
+ invalid?: boolean;
21
+ nativeSubmit?: boolean;
22
+ upload?: UploadHandler;
23
+ cancelUpload?: UploadCancelHandler;
24
+ onUploadStateChange?: (state: { busy: boolean; unresolved: boolean }) => void;
25
+ fileReferenceMode?: boolean;
26
+ fileScopeKey?: string | number | object;
27
+ }
28
+
29
+ let {
30
+ field, value, onchange, disabled = false, labelledby, describedby, invalid = false,
31
+ nativeSubmit = true, upload, cancelUpload, onUploadStateChange, fileReferenceMode = false,
32
+ fileScopeKey,
33
+ }: Props = $props();
16
34
 
17
- const arrayVal = $derived(Array.isArray(value) ? value : []);
35
+ const instanceId = $props.id();
36
+ const headingId = `${instanceId}-heading`;
37
+ const arrayVal = $derived<Record<string, unknown>[]>(Array.isArray(value) ? value : []);
18
38
  const subFields = $derived(field.subFields || []);
39
+ let previousItems: Record<string, unknown>[] = [];
40
+ let previousKeys: number[] = [];
41
+ const pendingEdits = new Set<number>();
42
+ let pendingItems: Record<string, unknown>[] = [];
43
+ let pendingKeys: number[] = [];
44
+ const childUploadStates = new Map<string, { busy: boolean; unresolved: boolean }>();
45
+ let outputSequence = 0;
46
+ let nextKey = 0;
47
+ function rememberOutput(items: Record<string, unknown>[], keys: number[]) {
48
+ Object.defineProperty(items, outputOrderKey, { value: ++outputSequence });
49
+ pendingItems = [...items];
50
+ pendingKeys = [...keys];
51
+ }
52
+ const rows = $derived.by(() => {
53
+ const acceptedOutput = Reflect.get(arrayVal, outputOrderKey) === outputSequence
54
+ && pendingItems.length === arrayVal.length
55
+ && arrayVal.every((item, index) => Object.is(item, pendingItems[index]));
56
+ if (acceptedOutput) {
57
+ const nextRows = arrayVal.map((item, index) => ({ item, key: pendingKeys[index] ?? ++nextKey }));
58
+ previousItems = [...arrayVal];
59
+ previousKeys = nextRows.map(row => row.key);
60
+ pendingItems = [];
61
+ pendingKeys = [];
62
+ pendingEdits.clear();
63
+ return nextRows;
64
+ }
65
+ const used = new Set<number>();
66
+ // 先保留显式编辑行,避免同一对象出现在多行时被未编辑的行抢走身份。
67
+ const matches = arrayVal.map(item => {
68
+ const key: unknown = Reflect.get(item, editedRowKey);
69
+ if (typeof key !== 'number' || !pendingEdits.has(key)) return -1;
70
+ const index = previousKeys.indexOf(key);
71
+ if (index < 0 || used.has(index)) return -1;
72
+ used.add(index);
73
+ return index;
74
+ });
75
+ arrayVal.forEach((item, position) => {
76
+ if (matches[position] !== -1) return;
77
+ const index = previousItems.findIndex((previous, i) => previous === item && !used.has(i));
78
+ if (index >= 0) used.add(index);
79
+ matches[position] = index;
80
+ });
81
+ const nextRows = arrayVal.map((item, index) => {
82
+ const match = matches[index] ?? -1;
83
+ const key = match >= 0 ? previousKeys[match] : undefined;
84
+ if (key !== undefined) pendingEdits.delete(key);
85
+ return { item, key: key ?? ++nextKey };
86
+ });
87
+ previousItems = [...arrayVal];
88
+ previousKeys = nextRows.map(row => row.key);
89
+ pendingItems = [];
90
+ pendingKeys = [];
91
+ pendingEdits.clear();
92
+ return nextRows;
93
+ });
19
94
 
20
95
  function getDefaultForType(type: string): unknown {
21
96
  switch (type) {
@@ -29,42 +104,121 @@
29
104
  }
30
105
 
31
106
  function handleAdd() {
107
+ if (disabled) return;
32
108
  const newItem: Record<string, unknown> = {};
33
109
  for (const f of subFields) {
34
110
  newItem[f.key] = f.defaultValue ?? getDefaultForType(f.type);
35
111
  }
36
- onchange([...arrayVal, newItem]);
112
+ const next = [...arrayVal, newItem];
113
+ rememberOutput(next, [...rows.map(row => row.key), ++nextKey]);
114
+ onchange(next);
115
+ }
116
+
117
+ function reportChildUploadState(
118
+ key: string,
119
+ state: { busy: boolean; unresolved: boolean },
120
+ ): void {
121
+ if (!state.busy && !state.unresolved) childUploadStates.delete(key);
122
+ else childUploadStates.set(key, state);
123
+ const states = [...childUploadStates.values()];
124
+ onUploadStateChange?.({
125
+ busy: states.some(value => value.busy),
126
+ unresolved: states.some(value => value.unresolved),
127
+ });
37
128
  }
38
129
 
39
130
  function handleRemove(index: number) {
40
- onchange(arrayVal.filter((_, i) => i !== index));
131
+ if (disabled) return;
132
+ const next = arrayVal.filter((_, i) => i !== index);
133
+ rememberOutput(next, rows.filter((_, i) => i !== index).map(row => row.key));
134
+ onchange(next);
135
+ }
136
+
137
+ function handleMove(index: number, direction: -1 | 1) {
138
+ if (disabled || index < 0 || index >= arrayVal.length) return;
139
+ const target = index + direction;
140
+ if (target < 0 || target >= arrayVal.length) return;
141
+ const next = [...arrayVal];
142
+ const moved = next.splice(index, 1);
143
+ next.splice(target, 0, ...moved);
144
+ const nextKeys = rows.map(row => row.key);
145
+ const movedKey = nextKeys.splice(index, 1);
146
+ nextKeys.splice(target, 0, ...movedKey);
147
+ rememberOutput(next, nextKeys);
148
+ onchange(next);
41
149
  }
42
150
 
43
151
  function handleChange(index: number, key: string, val: unknown) {
152
+ if (disabled) return;
44
153
  const next = [...arrayVal];
45
- next[index] = { ...next[index], [key]: val };
154
+ const edited = { ...next[index], [key]: val };
155
+ const rowKey = rows[index]?.key;
156
+ if (rowKey !== undefined) {
157
+ Object.defineProperty(edited, editedRowKey, { value: rowKey });
158
+ pendingEdits.add(rowKey);
159
+ }
160
+ next[index] = edited;
161
+ rememberOutput(next, rows.map(row => row.key));
46
162
  onchange(next);
47
163
  }
48
164
  </script>
49
165
 
50
- <div class="svadmin-u-3e7ce58d64fa svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-a29b7a649c77 svadmin-u-8e63407b5ceb">
166
+ <div
167
+ role="group"
168
+ aria-labelledby={labelledby ?? headingId}
169
+ aria-describedby={describedby}
170
+ aria-disabled={disabled || undefined}
171
+ data-invalid={invalid || undefined}
172
+ class="svadmin-u-3e7ce58d64fa svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-a29b7a649c77 svadmin-u-8e63407b5ceb"
173
+ >
51
174
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc">
52
- <div class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
175
+ <div id={headingId} class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
53
176
  {field.label}
54
177
  {#if field.required}
55
178
  <span class="svadmin-u-811148b13d1e">*</span>
56
179
  {/if}
57
180
  </div>
58
- <Button variant="outline" size="sm" type="button" onclick={handleAdd}>
181
+ <Button
182
+ variant="outline"
183
+ size="sm"
184
+ type="button"
185
+ disabled={disabled}
186
+ {...(describedby === undefined ? {} : { 'aria-describedby': describedby })}
187
+ {...(invalid ? { 'aria-invalid': true } : {})}
188
+ onclick={handleAdd}
189
+ >
59
190
  <Plus class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-618162408e7a" /> {i18n.t('common.add') || 'Add'}
60
191
  </Button>
61
192
  </div>
62
193
 
63
194
  <div class="svadmin-u-3e7ce58d64fa svadmin-u-f46b61a9b310">
64
- {#each arrayVal as item, i (i)}
65
- <div class="svadmin-u-d89972fe17d6 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-cd0ad9a56558 svadmin-u-8e63407b5ceb svadmin-u-30c1d058f0db svadmin-u-438b2237b8d6">
66
- <div class="svadmin-u-da4dbfbc4fdc svadmin-u-7b2d63937d23 svadmin-u-9a2db8f949b6">
67
- <TooltipButton tooltip={i18n.t('common.remove') || 'Remove'} variant="ghost" size="icon" class="svadmin-u-ed8a5df7b2fb svadmin-u-2bbcfc3b5179 svadmin-u-bfa603190748 svadmin-u-51e95020d6f2 svadmin-u-ceb69a6b0e5f" onclick={() => handleRemove(i)}>
195
+ {#each rows as row, i (row.key)}
196
+ {@const item = row.item}
197
+ <div role="group" aria-label={`${field.label} ${i + 1}`} class="svadmin-u-d89972fe17d6 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-cd0ad9a56558 svadmin-u-8e63407b5ceb svadmin-u-30c1d058f0db svadmin-u-438b2237b8d6">
198
+ <div style="display: flex; justify-content: flex-end; gap: 4px;">
199
+ <TooltipButton
200
+ disabled={disabled || i === 0}
201
+ tooltip={i18n.t('common.moveUp') || 'Move up'}
202
+ variant="ghost"
203
+ size="icon"
204
+ aria-label={i18n.t('common.moveUp') || 'Move up'}
205
+ class="svadmin-u-ed8a5df7b2fb svadmin-u-2bbcfc3b5179"
206
+ onclick={() => handleMove(i, -1)}
207
+ >
208
+ <ArrowUp class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
209
+ </TooltipButton>
210
+ <TooltipButton
211
+ disabled={disabled || i === arrayVal.length - 1}
212
+ tooltip={i18n.t('common.moveDown') || 'Move down'}
213
+ variant="ghost"
214
+ size="icon"
215
+ aria-label={i18n.t('common.moveDown') || 'Move down'}
216
+ class="svadmin-u-ed8a5df7b2fb svadmin-u-2bbcfc3b5179"
217
+ onclick={() => handleMove(i, 1)}
218
+ >
219
+ <ArrowDown class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
220
+ </TooltipButton>
221
+ <TooltipButton disabled={disabled} tooltip={i18n.t('common.remove') || 'Remove'} variant="ghost" size="icon" class="svadmin-u-ed8a5df7b2fb svadmin-u-2bbcfc3b5179 svadmin-u-bfa603190748 svadmin-u-51e95020d6f2 svadmin-u-ceb69a6b0e5f" onclick={() => handleRemove(i)}>
68
222
  <Trash2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
69
223
  </TooltipButton>
70
224
  </div>
@@ -73,6 +227,16 @@
73
227
  <FieldRenderer
74
228
  field={subField}
75
229
  value={item[subField.key]}
230
+ disabled={disabled}
231
+ {nativeSubmit}
232
+ {fileReferenceMode}
233
+ {...(fileScopeKey === undefined ? {} : { fileScopeKey })}
234
+ {...(upload === undefined ? {} : { upload })}
235
+ {...(cancelUpload === undefined ? {} : { cancelUpload })}
236
+ {...(onUploadStateChange === undefined
237
+ ? {}
238
+ : { onUploadStateChange: (state: { busy: boolean; unresolved: boolean }) =>
239
+ reportChildUploadState(`${row.key}:${subField.key}`, state) })}
76
240
  onchange={(val) => handleChange(i, subField.key, val)}
77
241
  />
78
242
  {/each}
@@ -1,9 +1,23 @@
1
1
  import type { FieldDefinition } from '@svadmin/core';
2
- type $$ComponentProps = {
2
+ import type { UploadCancelHandler, UploadHandler } from '../file-upload-types.js';
3
+ interface Props {
3
4
  field: FieldDefinition;
4
5
  value: unknown;
5
6
  onchange: (val: unknown) => void;
6
- };
7
- declare const ArrayField: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ disabled?: boolean;
8
+ labelledby?: string;
9
+ describedby?: string;
10
+ invalid?: boolean;
11
+ nativeSubmit?: boolean;
12
+ upload?: UploadHandler;
13
+ cancelUpload?: UploadCancelHandler;
14
+ onUploadStateChange?: (state: {
15
+ busy: boolean;
16
+ unresolved: boolean;
17
+ }) => void;
18
+ fileReferenceMode?: boolean;
19
+ fileScopeKey?: string | number | object;
20
+ }
21
+ declare const ArrayField: import("svelte").Component<Props, {}, "">;
8
22
  type ArrayField = ReturnType<typeof ArrayField>;
9
23
  export default ArrayField;
@@ -0,0 +1,4 @@
1
+ export declare const arrayFieldRowKey: unique symbol;
2
+ export declare const arrayFieldOrderKey: unique symbol;
3
+ /** 仅剥离 UI 行身份;保留其他描述符,让 Core 继续拒绝非法业务数据。 */
4
+ export declare function withoutArrayFieldMetadata(value: unknown): unknown;
@@ -0,0 +1,36 @@
1
+ export const arrayFieldRowKey = Symbol('array-field-row');
2
+ export const arrayFieldOrderKey = Symbol('array-field-order');
3
+ /** 仅剥离 UI 行身份;保留其他描述符,让 Core 继续拒绝非法业务数据。 */
4
+ export function withoutArrayFieldMetadata(value) {
5
+ const copies = new Map();
6
+ function copy(input) {
7
+ if (typeof input !== 'object' || input === null)
8
+ return input;
9
+ const prototype = Object.getPrototypeOf(input);
10
+ if (!Array.isArray(input) && prototype !== Object.prototype && prototype !== null)
11
+ return input;
12
+ const previous = copies.get(input);
13
+ if (previous)
14
+ return previous;
15
+ const output = Array.isArray(input) ? [] : Object.create(prototype);
16
+ copies.set(input, output);
17
+ for (const key of Reflect.ownKeys(input)) {
18
+ if (key === arrayFieldRowKey || key === arrayFieldOrderKey)
19
+ continue;
20
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
21
+ if (!descriptor)
22
+ continue;
23
+ if ('value' in descriptor)
24
+ descriptor.value = copy(descriptor.value);
25
+ Object.defineProperty(output, key, descriptor);
26
+ }
27
+ return output;
28
+ }
29
+ try {
30
+ return copy(value);
31
+ }
32
+ catch {
33
+ // 由 Core 归一化拒绝,不能把无法读取的数据替换成可提交的空值。
34
+ return value;
35
+ }
36
+ }
@@ -0,0 +1,11 @@
1
+ import type { UploadItem } from './file-upload-types.js';
2
+ /** 上传成功后写入表单的 JSON 引用;保留服务端返回的全部标识。 */
3
+ export type UploadReference = Pick<UploadItem, 'url' | 'uploadId'>;
4
+ export declare function uploadReference(value: unknown): UploadReference | undefined;
5
+ export declare function uploadReferenceFor(item: Pick<UploadItem, 'status' | 'url' | 'uploadId'>): UploadReference | undefined;
6
+ /** 兼容历史字符串基线;只读取 plain object 自身数据属性。 */
7
+ export declare function normalizeUploadReferences(value: unknown): UploadReference[];
8
+ export declare function uploadReferenceKey(reference: UploadReference): string;
9
+ /** 纯文本回显:优先 URL,其次 uploadId,避免把引用渲染成可点击链接。 */
10
+ export declare function uploadReferenceLabel(reference: UploadReference): string;
11
+ export declare function mergeUploadReferences(baseline: UploadReference[], uploaded: UploadReference[]): UploadReference[];