@svadmin/ui 0.88.1 → 0.91.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 (171) 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 +263 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +135 -35
  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 +60 -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/Transfer.svelte +67 -5
  109. package/dist/components/Transfer.svelte.d.ts +4 -0
  110. package/dist/components/TreeSelect.svelte +337 -77
  111. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  112. package/dist/components/TreeTable.svelte +6 -1
  113. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  114. package/dist/components/UpdatePasswordPage.svelte +1 -1
  115. package/dist/components/VersionDiffViewer.svelte +9 -7
  116. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  117. package/dist/components/account/MemberDirectory.svelte +14 -10
  118. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  119. package/dist/components/content/AuditTimeline.svelte +8 -6
  120. package/dist/components/content/DataState.svelte +1 -1
  121. package/dist/components/content/FileList.svelte +44 -13
  122. package/dist/components/content/FileList.svelte.d.ts +4 -2
  123. package/dist/components/content/FileListRow.svelte +87 -0
  124. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  125. package/dist/components/content/ImportDropzone.svelte +109 -12
  126. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  127. package/dist/components/content/IntegrationCard.svelte +3 -3
  128. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  129. package/dist/components/content/MediaThumbnail.svelte +6 -2
  130. package/dist/components/detail-workspace.d.ts +60 -0
  131. package/dist/components/detail-workspace.js +3 -0
  132. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  133. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  134. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  135. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  136. package/dist/components/enterprise/schema-form-references.js +316 -0
  137. package/dist/components/fields/ArrayField.svelte +178 -14
  138. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  139. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  140. package/dist/components/fields/array-field-metadata.js +36 -0
  141. package/dist/components/file-upload-reference.d.ts +11 -0
  142. package/dist/components/file-upload-reference.js +50 -0
  143. package/dist/components/file-upload-types.d.ts +32 -0
  144. package/dist/components/file-upload-types.js +1 -0
  145. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  147. package/dist/components/json-schema-form-state.d.ts +19 -0
  148. package/dist/components/list-deletion.svelte.js +6 -1
  149. package/dist/components/list-state.svelte.d.ts +12 -1
  150. package/dist/components/list-state.svelte.js +56 -16
  151. package/dist/components/numeric-format.d.ts +44 -0
  152. package/dist/components/numeric-format.js +198 -0
  153. package/dist/components/record-detail.svelte.js +2 -0
  154. package/dist/components/saved-list-views.d.ts +9 -2
  155. package/dist/components/saved-list-views.js +42 -11
  156. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  157. package/dist/components/saved-list-views.svelte.js +56 -7
  158. package/dist/components/task-action.svelte.d.ts +1 -0
  159. package/dist/components/task-action.svelte.js +55 -3
  160. package/dist/components/task-utils.d.ts +1 -0
  161. package/dist/components/task-utils.js +18 -0
  162. package/dist/components/ui/input/input.svelte +1 -1
  163. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  164. package/dist/components/ui/sheet/sheet.svelte +4 -1
  165. package/dist/components/version-history.d.ts +14 -0
  166. package/dist/components/version-history.js +32 -0
  167. package/dist/components/workspace-tab-state.d.ts +78 -0
  168. package/dist/components/workspace-tab-state.js +587 -0
  169. package/dist/index.d.ts +13 -1
  170. package/dist/index.js +4 -0
  171. package/package.json +5 -5
@@ -2,8 +2,8 @@
2
2
  import { definedReactiveOptions } from '@svadmin/core/options';
3
3
 
4
4
  import { fade } from 'svelte/transition';
5
- import { useInfiniteList, useResourceContract } from '@svadmin/core';
6
- import type { BaseRecord, Sort, Filter } from '@svadmin/core';
5
+ import { captureAdminContext, useInfiniteList, useResourceContract } from '@svadmin/core';
6
+ import type { BaseRecord, DataProvider, Sort, Filter } from '@svadmin/core';
7
7
  import type { Snippet } from 'svelte';
8
8
  import { useTranslation } from '@svadmin/core/i18n';
9
9
 
@@ -37,9 +37,12 @@
37
37
  loadingSkeleton,
38
38
  }: Props = $props();
39
39
 
40
+ const context = captureAdminContext();
40
41
  const binding = useResourceContract(() => resource);
41
42
  const { query } = useInfiniteList(definedReactiveOptions({
42
43
  get resource() { return binding.resource; },
44
+ // 查询错误由列表内的错误提示和重试按钮承接。
45
+ errorNotification: false,
43
46
  get dataProviderName() { return binding.dataProviderName; },
44
47
  get meta() { return binding.meta; },
45
48
  get pagination() { return { pageSize }; },
@@ -53,12 +56,44 @@
53
56
 
54
57
  const hasNextPage = $derived(query.hasNextPage ?? false);
55
58
  const isFetchingNextPage = $derived(query.isFetchingNextPage ?? false);
59
+ const isFetchNextPageError = $derived(query.isFetchNextPageError && query.data !== undefined);
60
+ const isBlockingError = $derived(
61
+ query.isError && (query.data === undefined || !query.isFetchNextPageError)
62
+ );
56
63
 
57
64
  function loadMore() {
58
- if (hasNextPage && !isFetchingNextPage) {
59
- query.fetchNextPage();
65
+ if (hasNextPage && !query.isFetching && !query.isError) {
66
+ void query.fetchNextPage({ cancelRefetch: false });
60
67
  }
61
68
  }
69
+
70
+ function itemKey(item: BaseRecord & { id: string | number }): string {
71
+ return `${typeof item.id}:${String(item.id)}`;
72
+ }
73
+
74
+ // 同名 Provider 替换也属于新作用域,不能仅按名称复用行草稿。
75
+ const providerIds = new WeakMap<DataProvider, number>();
76
+ let nextProviderId = 0;
77
+ const rowScope = $derived.by(() => {
78
+ const providerName = binding.dataProviderName;
79
+ const provider = context.providers?.[providerName];
80
+ let providerId = provider === undefined ? undefined : providerIds.get(provider);
81
+ if (provider !== undefined && providerId === undefined) {
82
+ providerId = ++nextProviderId;
83
+ providerIds.set(provider, providerId);
84
+ }
85
+ return JSON.stringify([resource, providerName, context.tenantCacheKey, providerId]);
86
+ });
87
+
88
+ const keyedItems = $derived.by(() => {
89
+ const seen = new Map<string, number>();
90
+ return allItems.map(item => {
91
+ const base = itemKey(item);
92
+ const occurrence = seen.get(base) ?? 0;
93
+ seen.set(base, occurrence + 1);
94
+ return { item, key: `${rowScope}:${base}:${occurrence}` };
95
+ });
96
+ });
62
97
  </script>
63
98
 
64
99
  <div class="svadmin-u-6f7e013d6499">
@@ -72,7 +107,7 @@
72
107
  {/each}
73
108
  </div>
74
109
  {/if}
75
- {:else if query.isError}
110
+ {:else if isBlockingError}
76
111
  <div role="alert">
77
112
  <p>{i18n.t('common.error')}</p>
78
113
  <Button variant="outline" disabled={query.isFetching} onclick={() => query.refetch()}>
@@ -87,7 +122,8 @@
87
122
  <p class="svadmin-u-ca6bf63030aa svadmin-u-fc7473ca09eb svadmin-u-bfa603190748 svadmin-u-a1f611f027dd">{i18n.t('empty.title')}</p>
88
123
  {/if}
89
124
  {:else}
90
- {#each allItems as item, index (index)}
125
+ {#each keyedItems as entry, index (entry.key)}
126
+ {@const item = entry.item}
91
127
  <div in:fade={{ duration: 200, delay: index < 20 ? index * 30 : 0 }}>
92
128
  {@render children({ item, index })}
93
129
  </div>
@@ -97,9 +133,16 @@
97
133
  <div class="svadmin-u-60fbb7713999 svadmin-u-86843cf1e227 svadmin-u-cb11fec3bb46">
98
134
  <Loader2 class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e svadmin-u-afbdd13a380e svadmin-u-bfa603190748" />
99
135
  </div>
100
- {/if}
101
-
102
- {#if hasNextPage}
136
+ {:else if isFetchNextPageError}
137
+ <div role="alert">
138
+ <p>{i18n.t('common.error')}</p>
139
+ <Button type="button" variant="outline" disabled={query.isFetching}
140
+ onclick={() => query.fetchNextPage({ cancelRefetch: false })}>
141
+ <RotateCw />
142
+ {i18n.t('common.retry')}
143
+ </Button>
144
+ </div>
145
+ {:else if hasNextPage}
103
146
  <!-- Sentinel for infinite scroll -->
104
147
  <div use:intersect={loadMore} class="svadmin-u-3a1268a4e17f"></div>
105
148
  {:else}
@@ -11,13 +11,19 @@
11
11
  schemaFormSnapshot, writeSchemaFormPath,
12
12
  type JsonSchemaFormSchema,
13
13
  } from './json-schema-form-state.js';
14
- import { readRecursiveSchemaForm, validateRecursiveSchemaForm, schemaFormPointer } from './enterprise/recursive-schema-form.js';
15
- import { parseSchemaNumber, type SchemaFormIssue } from './enterprise/json-schema-form.js';
14
+ import {
15
+ readRecursiveSchemaForm, validateRecursiveSchemaForm, schemaFormPointer,
16
+ pruneRecursiveSchemaFormValue, selectRecursiveOneOfBranch, switchRecursiveOneOfBranch,
17
+ type RecursiveSchemaFormNode,
18
+ } from './enterprise/recursive-schema-form.js';
19
+ import { resolveSchemaFormReferences, type SchemaFormReferenceDocuments } from './enterprise/schema-form-references.js';
20
+ import { parseSchemaNumber, type JsonScalar, type SchemaFormIssue } from './enterprise/json-schema-form.js';
16
21
  interface JsonSchema extends JsonSchemaFormSchema {
17
22
  properties?: Record<string, JsonSchema>;
18
23
  items?: JsonSchema;
19
- visibleWhen?: SchemaFormSchema['visibleWhen'];
20
- pattern?: string;
24
+ oneOf?: JsonSchema[];
25
+ discriminator?: { propertyName: string };
26
+ $defs?: Record<string, JsonSchema>;
21
27
  }
22
28
 
23
29
  interface Props {
@@ -31,6 +37,10 @@
31
37
  locale?: string;
32
38
  onerror?: (error: unknown) => void;
33
39
  onvalidationerror?: (issues: SchemaFormIssue[]) => void;
40
+ /** Schema 配置本身不可用时调用;与值校验 onvalidationerror 分离。 */
41
+ onschemaerror?: (issues: SchemaFormIssue[]) => void;
42
+ /** 宿主已取得的普通 Schema 文档;键必须与 `$ref` 的 URI 精确匹配。 */
43
+ referenceDocuments?: SchemaFormReferenceDocuments;
34
44
  /** Use a unique stable prefix when several schema forms share a page. */
35
45
  idPrefix?: string;
36
46
  class?: string;
@@ -46,6 +56,8 @@
46
56
  locale,
47
57
  onerror,
48
58
  onvalidationerror,
59
+ onschemaerror,
60
+ referenceDocuments = {},
49
61
  idPrefix,
50
62
  class: className = '',
51
63
  }: Props = $props();
@@ -57,12 +69,19 @@
57
69
  const labels = $derived(isZh ? { add: '添加项目', remove: '删除', empty: '暂无项目', failed: '提交未完成,请检查状态后重试。', invalid: '请检查表单输入。' }
58
70
  : { add: 'Add item', remove: 'Remove item', empty: 'No items', failed: 'Submission did not complete. Check its status before retrying.', invalid: 'Check the form input.' });
59
71
  let isSubmitting = $state(false);
60
- const model = $derived(readRecursiveSchemaForm(schema));
72
+ // 引用只展开一次;递归编译、默认值、渲染、条件显示、只读与提交共用同一结果。
73
+ const resolution = $derived(resolveSchemaFormReferences(schema, referenceDocuments));
74
+ const resolvedSchema = $derived(resolution.schema);
75
+ const model = $derived(readRecursiveSchemaForm(resolvedSchema));
61
76
  // 两个已发布的 schema 子集分别验证;任何一个都不接受时关闭表单。
62
77
  const coreSchema = $derived.by(() => {
63
- try { assertSchemaFormSchema(schema as SchemaFormSchema); return schema as SchemaFormSchema; }
78
+ if (!resolvedSchema) return undefined;
79
+ try { assertSchemaFormSchema(resolvedSchema as SchemaFormSchema); return resolvedSchema as SchemaFormSchema; }
64
80
  catch { return undefined; }
65
81
  });
82
+ const configurationIssues = $derived(resolution.issues.length
83
+ ? resolution.issues
84
+ : model.issues.length && !coreSchema ? model.issues : []);
66
85
  let validationIssues = $state<SchemaFormIssue[]>([]);
67
86
  let parseIssues = $state<SchemaFormIssue[]>([]);
68
87
  let failure = $state<'invalid' | 'failed' | null>(null);
@@ -86,10 +105,30 @@
86
105
  return undefined;
87
106
  }
88
107
 
108
+ /** Resolve a branch only when the data already carries one unique explicit discriminator value. */
109
+ function selectOneOfSchema(node: JsonSchema, current: unknown): JsonSchema | undefined {
110
+ const propertyName = node.discriminator?.propertyName;
111
+ if (!propertyName || !node.oneOf?.length) return undefined;
112
+ const value = recordValue(current, propertyName);
113
+ if (value === undefined) return undefined;
114
+ const matched = node.oneOf.filter(branch => {
115
+ const selector = branch?.properties?.[propertyName];
116
+ const values = selector ? choices(selector) : undefined;
117
+ return values?.some(choice => Object.is(choice, value)) ?? false;
118
+ });
119
+ return matched.length === 1 ? matched[0] : undefined;
120
+ }
121
+
89
122
  function mergeDefaults(node: JsonSchema, current: unknown, present: boolean, budget = { nodes: 0 }): unknown {
90
123
  if (++budget.nodes > 1000) throw new Error('Schema form data limit exceeded.');
91
124
  const candidate = present ? current : cloneDefault(node);
92
125
  if (candidate === null || (present && candidate === undefined)) return candidate;
126
+ // 只对已显式携带 discriminator 的分支补默认值;未选择或歧义时绝不猜测分支。
127
+ if (node.oneOf && node.discriminator) {
128
+ const branch = selectOneOfSchema(node, candidate);
129
+ if (!branch) return candidate;
130
+ return mergeDefaults(branch, candidate, true, budget);
131
+ }
93
132
  if (node.type === 'array') {
94
133
  if (Array.isArray(candidate) && candidate.length > 1000 - budget.nodes) throw new Error('Schema form data limit exceeded.');
95
134
  return Array.isArray(candidate)
@@ -110,14 +149,32 @@
110
149
 
111
150
  const prepared = $derived.by(() => {
112
151
  try {
113
- if (model.issues.length && !coreSchema) return { ok: false as const };
114
- const seeded = mergeDefaults(schema, value, true);
115
- return { ok: true as const, value: initializeSchemaForm(schema, seeded as Record<string, unknown>) };
152
+ if (configurationIssues.length || !resolvedSchema) return { ok: false as const };
153
+ const seeded = mergeDefaults(resolvedSchema as JsonSchema, value, true);
154
+ return { ok: true as const, value: initializeSchemaForm(resolvedSchema as JsonSchema, seeded as Record<string, unknown>) };
116
155
  } catch {
117
156
  return { ok: false as const };
118
157
  }
119
158
  });
120
159
  const locked = $derived(disabled || readonly || isSubmitting || !prepared.ok);
160
+ let reportedConfiguration = '';
161
+ let reportedSchema: JsonSchema | undefined;
162
+ $effect.pre(() => {
163
+ const issues = configurationIssues;
164
+ if (!issues.length) {
165
+ reportedConfiguration = '';
166
+ reportedSchema = schema;
167
+ return;
168
+ }
169
+ const fingerprint = issues.map(issue => `${issue.code}@${issue.path}`).join('|');
170
+ if (schema === reportedSchema && fingerprint === reportedConfiguration) return;
171
+ reportedConfiguration = fingerprint;
172
+ reportedSchema = schema;
173
+ untrack(() => {
174
+ try { onschemaerror?.(issues.map(issue => ({ ...issue }))); }
175
+ catch { /* 配置观察者不参与可用性判定。 */ }
176
+ });
177
+ });
121
178
  $effect.pre(() => {
122
179
  const next = [schema, value, disabled, readonly, onsubmit];
123
180
  untrack(() => {
@@ -143,14 +200,59 @@
143
200
  function choices(node: JsonSchema): JsonSchema['enum'] {
144
201
  return node.enum ?? (Object.hasOwn(node, 'const') ? [node.const ?? null] : undefined);
145
202
  }
203
+ function childValue(current: unknown, key: string): unknown {
204
+ if (Array.isArray(current)) {
205
+ if (!/^(0|[1-9]\d*)$/u.test(key)) return undefined;
206
+ // 数组索引必须按自身数据属性读取;recordValue 对数组刻意返回 undefined。
207
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
208
+ return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;
209
+ }
210
+ return recordValue(current, key);
211
+ }
146
212
  function pathReadonly(path: readonly string[]): boolean {
147
- let node: JsonSchema | undefined = schema;
213
+ let node: JsonSchema | undefined = resolvedSchema;
214
+ let current: unknown = prepared.ok ? prepared.value : value;
215
+ if (!node) return false;
216
+ // 每层先检查自身 readOnly,再按 discriminator 解析分支,避免根 oneOf 丢弃自身约束。
148
217
  if (node.readOnly) return true;
218
+ const rootBranch = selectOneOfSchema(node, current);
219
+ if (rootBranch) {
220
+ node = rootBranch;
221
+ if (node.readOnly) return true;
222
+ }
149
223
  for (const key of path) {
150
- node = node && (nodeType(node) === 'array' ? node.items : node.properties?.[key]);
224
+ if (!node) return false;
225
+ const branch = selectOneOfSchema(node, current);
226
+ if (branch) {
227
+ node = branch;
228
+ if (node.readOnly) return true;
229
+ }
230
+ node = nodeType(node) === 'array' ? node.items : node.properties?.[key];
231
+ current = childValue(current, key);
151
232
  if (node?.readOnly) return true;
152
233
  }
153
- return false;
234
+ if (!node) return false;
235
+ const selected = selectOneOfSchema(node, current);
236
+ return selected?.readOnly === true;
237
+ }
238
+ /** discriminator 属性若声明 readOnly,只锁定分支选择器,不冻结同分支的其他可编辑字段。 */
239
+ function discriminatorLocked(path: readonly string[]): boolean {
240
+ let node: JsonSchema | undefined = resolvedSchema;
241
+ let current: unknown = prepared.ok ? prepared.value : value;
242
+ for (const key of path) {
243
+ if (!node) return false;
244
+ const branch = selectOneOfSchema(node, current);
245
+ if (branch) node = branch;
246
+ node = nodeType(node) === 'array' ? node.items : node.properties?.[key];
247
+ current = childValue(current, key);
248
+ }
249
+ if (!node) return false;
250
+ const propertyName = node.discriminator?.propertyName;
251
+ if (!propertyName) return false;
252
+ const selected = selectOneOfSchema(node, current);
253
+ // 未选中分支时,只有所有候选分支都只读才保护选择器,避免误冻结其他可编辑配置。
254
+ const branches = selected ? [selected] : node.oneOf ?? [];
255
+ return branches.length > 0 && branches.every(branch => branch.properties?.[propertyName]?.readOnly === true);
154
256
  }
155
257
  function invalid(issues: SchemaFormIssue[]): void {
156
258
  failure = 'invalid';
@@ -160,6 +262,19 @@
160
262
  function fieldInvalid(path: readonly string[]): boolean {
161
263
  return [...validationIssues, ...parseIssues].some(issue => issue.path === schemaFormPointer(path));
162
264
  }
265
+ function isVisibleIssuePath(pointer: string): boolean {
266
+ if (!coreSchema || !prepared.ok || !pointer.startsWith('/')) return true;
267
+ const path = pointer.slice(1).split('/').map(segment => segment.replaceAll('~1', '/').replaceAll('~0', '~'));
268
+ let node: JsonSchema | undefined = resolvedSchema;
269
+ for (const segment of path) {
270
+ if (!node) return true;
271
+ const kind = nodeType(node);
272
+ node = kind === 'array' ? node.items : node.properties?.[segment];
273
+ if (!node) return true;
274
+ if (!isSchemaFormNodeVisible(node as SchemaFormSchema, prepared.value)) return false;
275
+ }
276
+ return true;
277
+ }
163
278
  function writeNumber(path: string[], input: HTMLInputElement): void {
164
279
  if (!editable() || pathReadonly(path)) return;
165
280
  const pointer = schemaFormPointer(path);
@@ -177,11 +292,23 @@
177
292
  current && typeof current === 'object' && Object.hasOwn(current, key) ? (current as Record<string, unknown>)[key] : undefined
178
293
  ), prepared.ok ? prepared.value : {});
179
294
  }
295
+ function recordValue(current: unknown, key: string): unknown {
296
+ if (current === null || typeof current !== 'object' || Array.isArray(current) || !Object.hasOwn(current, key)) return undefined;
297
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
298
+ return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;
299
+ }
300
+ function oneOfOptionIndex(choices: readonly JsonScalar[][], current: unknown): string {
301
+ for (const [branch, values] of choices.entries()) {
302
+ const valueIndex = values.findIndex(value => Object.is(value, current));
303
+ if (valueIndex >= 0) return `${branch}:${valueIndex}`;
304
+ }
305
+ return '';
306
+ }
180
307
 
181
308
  function writePath(path: string[], nextValue: unknown): boolean {
182
309
  if (!editable() || !prepared.ok || pathReadonly(path)) return false;
183
310
  try {
184
- value = writeSchemaFormPath(prepared.value, path, nextValue);
311
+ value = path.length ? writeSchemaFormPath(prepared.value, path, nextValue) : schemaFormSnapshot(nextValue);
185
312
  internalValue = value;
186
313
  failure = null;
187
314
  validationIssues = [];
@@ -234,9 +361,13 @@
234
361
  if (!editable() || !prepared.ok || !onsubmit) return;
235
362
  let payload: Record<string, unknown>;
236
363
  try {
237
- const result = coreSchema ? prepareSchemaFormValue(coreSchema, prepared.value) : undefined;
238
- const detailedIssues = model.issues.length ? [] : validateRecursiveSchemaForm(model, prepared.value);
239
- const problems = [...parseIssues, ...(result
364
+ // 分支切换后只提交当前 discriminator 对应分支;旧分支字段不会随草稿误提交。
365
+ const submittedValue = pruneRecursiveSchemaFormValue(model, prepared.value);
366
+ const result = coreSchema ? prepareSchemaFormValue(coreSchema, submittedValue) : undefined;
367
+ const detailedIssues = model.issues.length ? [] : validateRecursiveSchemaForm(model, submittedValue);
368
+ // 隐藏字段暂不参与校验,但保留解析错误,重新显示时仍需修正。
369
+ const activeParseIssues = parseIssues.filter(issue => isVisibleIssuePath(issue.path));
370
+ const problems = [...activeParseIssues, ...(result
240
371
  ? result.errors.map(issue => {
241
372
  const path = schemaFormPointer(JSON.parse(issue.path) as string[]);
242
373
  // 共用子集保留公开错误码;核心校验仍决定哪些字段不可提交。
@@ -249,7 +380,7 @@
249
380
  formElement?.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus();
250
381
  return;
251
382
  }
252
- payload = schemaFormSnapshot(result?.data ?? prepared.value);
383
+ payload = schemaFormSnapshot(result?.data ?? submittedValue);
253
384
  } catch { invalid([{ path: '', code: 'invalid-value' }]); return; }
254
385
  const currentValue = value, currentSchema = schema, handler = onsubmit, revision = authorityRevision;
255
386
  const fingerprint = JSON.stringify(schemaFormSnapshot(prepared.value));
@@ -281,10 +412,10 @@
281
412
  onsubmit={handleSubmit}
282
413
  class={cn('svadmin-u-3e7ce58d64fa svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-0478c89a150f svadmin-u-cef5b893cf23 svadmin-u-359090c2d529', className)}
283
414
  >
284
- {#if schema.title}
415
+ {#if resolvedSchema?.title}
285
416
  <div class="svadmin-u-f4cc511ff0c1 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff">
286
- <h3 class="svadmin-u-fc7473ca09eb svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{schema.title}</h3>
287
- {#if schema.description}<p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{schema.description}</p>{/if}
417
+ <h3 class="svadmin-u-fc7473ca09eb svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{resolvedSchema.title}</h3>
418
+ {#if resolvedSchema.description}<p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{resolvedSchema.description}</p>{/if}
288
419
  </div>
289
420
  {/if}
290
421
 
@@ -292,34 +423,95 @@
292
423
  <p role="alert" data-testid="schema-form-errors">{labels[!prepared.ok ? 'invalid' : failure ?? 'invalid']}</p>
293
424
  {/if}
294
425
 
295
- {#snippet renderField(node: JsonSchema, path: string[], title: string, required = false)}
426
+ {#snippet renderField(node: JsonSchema, compiled: RecursiveSchemaFormNode | undefined, path: string[], title: string, required = false)}
296
427
  {@const current = readPath(path)}
297
428
  {@const id = `${prefix}_${path.map(encodeURIComponent).join('/')}`}
298
429
  {@const type = nodeType(node)}
299
430
  {@const fieldLocked = locked || pathReadonly(path)}
300
431
  {@const error = fieldInvalid(path)}
301
432
  {@const options = choices(node)}
433
+ {@const descriptionId = node.description ? `${id}:description` : undefined}
434
+ {@const errorId = error ? `${id}:error` : undefined}
435
+ {@const oneOf = compiled?.kind === 'oneOf' ? compiled : undefined}
436
+ {@const oneOfSelection = oneOf ? selectRecursiveOneOfBranch(oneOf, current) : 'oneOf-unselected'}
437
+ {@const branchIndex = typeof oneOfSelection === 'number' ? oneOfSelection : -1}
438
+ {@const branchSchema = oneOf && branchIndex >= 0 ? node.oneOf?.[branchIndex] : undefined}
439
+ {@const branchCompiled = oneOf && branchIndex >= 0 ? oneOf.branches[branchIndex] : undefined}
440
+ {@const oneOfValue = oneOf ? recordValue(current, oneOf.propertyName) : undefined}
441
+ {@const selectorId = `${id}:branch`}
442
+ {@const selectorLocked = fieldLocked || discriminatorLocked(path)}
443
+ {@const selectionError = oneOf ? validationIssues.some(issue => issue.path === schemaFormPointer([...path, oneOf.propertyName]) || issue.path === schemaFormPointer([...path, 'oneOf'])) : false}
302
444
  {#if coreSchema && !isSchemaFormNodeVisible(node as SchemaFormSchema, prepared.ok ? prepared.value : {})}
303
445
  <!-- 隐藏字段保留草稿,但不进入提交数据。 -->
446
+ {:else if oneOf}
447
+ <fieldset disabled={fieldLocked} aria-describedby={[descriptionId, selectionError ? `${selectorId}:error` : undefined].filter(Boolean).join(' ') || undefined} class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
448
+ <legend class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069">{title}</legend>
449
+ {#if node.description}<p id={descriptionId} class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
450
+ <div class="svadmin-u-da7c36cd8867">
451
+ <label for={selectorId} class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
452
+ {oneOf.propertyName}{#if required}<span class="svadmin-u-811148b13d1e">*</span>{/if}
453
+ </label>
454
+ <select id={selectorId} disabled={selectorLocked} aria-invalid={selectionError ? 'true' : undefined} aria-describedby={selectionError ? `${selectorId}:error` : undefined} value={oneOfOptionIndex(oneOf.values, oneOfValue)} onchange={(event) => {
455
+ if (selectorLocked || !editable()) return;
456
+ const token = event.currentTarget.value;
457
+ if (!/^(0|[1-9]\d*):(0|[1-9]\d*)$/u.test(token)) return;
458
+ const [branchToken, valueToken] = token.split(':');
459
+ const branch = Number(branchToken), valueIndex = Number(valueToken);
460
+ const selected = oneOf.values[branch]?.[valueIndex];
461
+ const next = switchRecursiveOneOfBranch(oneOf, current ?? {}, branch, selected);
462
+ const branchSchema = next ? node.oneOf?.[branch] : undefined;
463
+ // 显式切换后才按目标分支播种默认值,绝不在未选择时猜测分支。
464
+ const seeded = branchSchema
465
+ ? mergeDefaults(branchSchema as JsonSchema, next, true) as Record<string, unknown>
466
+ : next;
467
+ if (next) writePath(path, seeded as Record<string, unknown>);
468
+ }} 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">
469
+ <option value="">{i18n.t('common.selectOption')}</option>
470
+ {#each oneOf.values as branchValues, branch (branch)}
471
+ {#each branchValues as value, valueIndex (valueIndex)}
472
+ <option value={`${branch}:${valueIndex}`}>{String(value)}</option>
473
+ {/each}
474
+ {/each}
475
+ </select>
476
+ {#if selectionError}<p id={`${selectorId}:error`}>{i18n.t('validation.invalidFormat')}</p>{/if}
477
+ </div>
478
+ {#if branchSchema && branchCompiled}
479
+ {#if branchCompiled.kind === 'object'}
480
+ <fieldset disabled={fieldLocked} class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
481
+ <legend class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069">{branchSchema.title ?? title}</legend>
482
+ {#if branchSchema.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{branchSchema.description}</p>{/if}
483
+ <div class="svadmin-u-9c6cdfa2ba3d">
484
+ {#each Object.entries(branchSchema.properties ?? {}) as [key, child] (key)}
485
+ {#if key !== oneOf.propertyName}
486
+ {@render renderField(child, branchCompiled.properties[key], [...path, key], child.title ?? key, branchSchema.required?.includes(key) ?? false)}
487
+ {/if}
488
+ {/each}
489
+ </div>
490
+ </fieldset>
491
+ {:else}
492
+ {@render renderField(branchSchema, branchCompiled, path, title, required)}
493
+ {/if}
494
+ {/if}
495
+ </fieldset>
304
496
  {:else if type === 'object' || node.properties}
305
- <fieldset disabled={fieldLocked} class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
497
+ <fieldset disabled={fieldLocked} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
306
498
  <legend class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069">{title}</legend>
307
- {#if error}<p id={`${id}-error`}>{i18n.t('validation.invalidFormat')}</p>{/if}
308
- {#if node.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
499
+ {#if error}<p id={errorId}>{i18n.t('validation.invalidFormat')}</p>{/if}
500
+ {#if node.description}<p id={descriptionId} class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
309
501
  <div class="svadmin-u-9c6cdfa2ba3d">
310
502
  {#each Object.entries(node.properties ?? {}) as [key, child] (key)}
311
- {@render renderField(child, [...path, key], child.title ?? key, node.required?.includes(key) ?? false)}
503
+ {@render renderField(child, compiled?.kind === 'object' ? compiled.properties[key] : undefined, [...path, key], child.title ?? key, node.required?.includes(key) ?? false)}
312
504
  {/each}
313
505
  </div>
314
506
  </fieldset>
315
507
  {:else if type === 'array'}
316
508
  {@const items = Array.isArray(current) ? current : []}
317
- <fieldset disabled={fieldLocked} class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
509
+ <fieldset disabled={fieldLocked} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
318
510
  <legend class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069">{title}</legend>
319
- {#if error}<p id={`${id}-error`}>{i18n.t('validation.invalidFormat')}</p>{/if}
320
- {#if node.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
511
+ {#if error}<p id={errorId}>{i18n.t('validation.invalidFormat')}</p>{/if}
512
+ {#if node.description}<p id={descriptionId} class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
321
513
  {#each items as _, index (index)}
322
- {@render renderField(node.items ?? { type: 'string' }, [...path, String(index)], `${title} ${index + 1}`)}
514
+ {@render renderField(node.items ?? { type: 'string' }, compiled?.kind === 'array' ? compiled.item : undefined, [...path, String(index)], `${title} ${index + 1}`)}
323
515
  <Button type="button" variant="ghost" size="sm" disabled={fieldLocked || (node.minItems !== undefined && items.length <= node.minItems)} onclick={() => removeArrayItem(path, index)}>
324
516
  <Trash2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" /> {labels.remove}
325
517
  </Button>
@@ -335,34 +527,37 @@
335
527
  <label for={id} class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
336
528
  {title}{#if required}<span class="svadmin-u-811148b13d1e">*</span>{/if}
337
529
  </label>
338
- {#if node.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
530
+ {#if node.description}<p id={descriptionId} class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
339
531
  {#if options}
340
- <select id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={error ? `${id}-error` : undefined} required={required} value={enumIndex(current, options) < 0 ? '' : enumToken(enumIndex(current, options))} onchange={(event) => writePath(path, enumValue(event.currentTarget.value, options))} 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">
532
+ <select id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} required={required} value={enumIndex(current, options) < 0 ? '' : enumToken(enumIndex(current, options))} onchange={(event) => writePath(path, enumValue(event.currentTarget.value, options))} 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">
341
533
  <option value="">{i18n.t('common.selectOption')}</option>
342
534
  {#each options as option, index (index)}<option value={enumToken(index)}>{String(option)}</option>{/each}
343
535
  </select>
344
536
  {:else if node.type === 'null'}
345
- <input id={id} disabled={fieldLocked} type="text" value="null" readonly />
537
+ <input id={id} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} type="text" value="null" readonly />
346
538
  {:else if type === 'boolean'}
347
- <input id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={error ? `${id}-error` : undefined} type="checkbox" checked={Boolean(current)} onchange={(event) => writePath(path, event.currentTarget.checked)} />
539
+ <input id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} type="checkbox" checked={Boolean(current)} onchange={(event) => writePath(path, event.currentTarget.checked)} />
348
540
  {:else if type === 'number' || type === 'integer'}
349
- <input id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={error ? `${id}-error` : undefined} type="number" step={type === 'integer' ? 1 : 'any'} min={node.minimum} max={node.maximum} required={required} value={current == null ? '' : String(current)} oninput={(event) => writeNumber(path, event.currentTarget)} 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" />
541
+ <input id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} type="number" step={type === 'integer' ? 1 : 'any'} min={node.minimum} max={node.maximum} required={required} value={current == null ? '' : String(current)} oninput={(event) => writeNumber(path, event.currentTarget)} 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" />
350
542
  {:else}
351
- <input id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={error ? `${id}-error` : undefined} type="text" minlength={node.minLength} maxlength={node.maxLength} required={required} value={String(current ?? '')} oninput={(event) => writePath(path, event.currentTarget.value)} 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" />
543
+ <input id={id} name={path.join('.')} disabled={fieldLocked} aria-invalid={error ? 'true' : undefined} aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} type="text" minlength={node.minLength} maxlength={node.maxLength} required={required} value={String(current ?? '')} oninput={(event) => writePath(path, event.currentTarget.value)} 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" />
352
544
  {/if}
353
- {#if error}<p id={`${id}-error`}>{i18n.t('validation.invalidFormat')}</p>{/if}
545
+ {#if error}<p id={errorId}>{i18n.t('validation.invalidFormat')}</p>{/if}
354
546
  </div>
355
547
  {/if}
356
548
  {/snippet}
357
549
 
358
550
  <fieldset class="schema-form-fields svadmin-u-3e7ce58d64fa" disabled={locked}>
359
551
  {#if prepared.ok}
360
- <div class="svadmin-u-9c6cdfa2ba3d">
361
- {#each Object.entries(schema.properties ?? {}) as [key, node] (key)}
362
- {@render renderField(node, [key], node.title ?? key, schema.required?.includes(key) ?? false)}
363
- {/each}
364
- </div>
365
-
552
+ {#if model.node?.kind === 'oneOf'}
553
+ {@render renderField(resolvedSchema as JsonSchema, model.node, [], resolvedSchema?.title ?? '', false)}
554
+ {:else}
555
+ <div class="svadmin-u-9c6cdfa2ba3d">
556
+ {#each Object.entries(resolvedSchema?.properties ?? {}) as [key, node] (key)}
557
+ {@render renderField(node, model.node?.kind === 'object' ? model.node.properties[key] : undefined, [key], node.title ?? key, resolvedSchema?.required?.includes(key) ?? false)}
558
+ {/each}
559
+ </div>
560
+ {/if}
366
561
  {/if}
367
562
  <div class="svadmin-u-173fa8f06789 svadmin-u-b950dda299d3 svadmin-u-05faf5c801ff svadmin-u-60fbb7713999 svadmin-u-77c08e015d14">
368
563
  <Button type="submit" size="sm" disabled={locked} class="svadmin-u-44ee8ba0a421 svadmin-u-25effcb585ab">
@@ -1,11 +1,14 @@
1
- import { type SchemaFormSchema } from '@svadmin/core/schema-form';
2
1
  import { type JsonSchemaFormSchema } from './json-schema-form-state.js';
2
+ import { type SchemaFormReferenceDocuments } from './enterprise/schema-form-references.js';
3
3
  import { type SchemaFormIssue } from './enterprise/json-schema-form.js';
4
4
  interface JsonSchema extends JsonSchemaFormSchema {
5
5
  properties?: Record<string, JsonSchema>;
6
6
  items?: JsonSchema;
7
- visibleWhen?: SchemaFormSchema['visibleWhen'];
8
- pattern?: string;
7
+ oneOf?: JsonSchema[];
8
+ discriminator?: {
9
+ propertyName: string;
10
+ };
11
+ $defs?: Record<string, JsonSchema>;
9
12
  }
10
13
  interface Props {
11
14
  schema: JsonSchema;
@@ -18,6 +21,10 @@ interface Props {
18
21
  locale?: string;
19
22
  onerror?: (error: unknown) => void;
20
23
  onvalidationerror?: (issues: SchemaFormIssue[]) => void;
24
+ /** Schema 配置本身不可用时调用;与值校验 onvalidationerror 分离。 */
25
+ onschemaerror?: (issues: SchemaFormIssue[]) => void;
26
+ /** 宿主已取得的普通 Schema 文档;键必须与 `$ref` 的 URI 精确匹配。 */
27
+ referenceDocuments?: SchemaFormReferenceDocuments;
21
28
  /** Use a unique stable prefix when several schema forms share a page. */
22
29
  idPrefix?: string;
23
30
  class?: string;