@svadmin/ui 0.88.0 → 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
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Minimal, explicit locale contract for the numeric input family.
3
+ *
4
+ * Only the locales below are accepted by the public `locale` prop. The default
5
+ * (no `locale`) keeps the legacy `type="number"` behaviour untouched. Adding a
6
+ * locale is a product decision and must come with its own parsing/screen-reader
7
+ * matrix instead of silently widening the contract.
8
+ */
9
+ export const NUMERIC_INPUT_LOCALES = ['en-US', 'de-DE', 'zh-CN'];
10
+ const MAX_INPUT_LENGTH = 64;
11
+ export function isNumericInputLocale(value) {
12
+ return typeof value === 'string' && NUMERIC_INPUT_LOCALES.includes(value);
13
+ }
14
+ const separatorCache = new Map();
15
+ function separatorsFor(locale) {
16
+ const cached = separatorCache.get(locale);
17
+ if (cached)
18
+ return cached;
19
+ const parts = new Intl.NumberFormat(locale, { useGrouping: true }).formatToParts(1234567.89);
20
+ const group = parts.find(part => part.type === 'group')?.value ?? ',';
21
+ const decimal = parts.find(part => part.type === 'decimal')?.value ?? '.';
22
+ const separators = { group, decimal };
23
+ separatorCache.set(locale, separators);
24
+ return separators;
25
+ }
26
+ export function clampNumericPrecision(precision) {
27
+ if (precision == null || !Number.isFinite(precision))
28
+ return undefined;
29
+ return Math.min(20, Math.max(0, Math.trunc(precision)));
30
+ }
31
+ /**
32
+ * Formats a stored value for display. `precision` only rounds the display; the
33
+ * caller keeps the original stored number. Intl's default rounding is used.
34
+ */
35
+ export function formatNumericValue(locale, value, precision) {
36
+ if (value == null || !Number.isFinite(value))
37
+ return '';
38
+ const digits = clampNumericPrecision(precision);
39
+ const options = {
40
+ useGrouping: true,
41
+ minimumFractionDigits: digits ?? 0,
42
+ maximumFractionDigits: digits ?? 20,
43
+ };
44
+ return new Intl.NumberFormat(locale, options).format(value);
45
+ }
46
+ /**
47
+ * Editable text mirrors formatting but omits grouping so that inserting a digit
48
+ * does not make an otherwise valid number look like a mixed-separator error.
49
+ */
50
+ export function formatNumericEditingText(locale, value, precision) {
51
+ if (value == null || !Number.isFinite(value))
52
+ return '';
53
+ const digits = clampNumericPrecision(precision);
54
+ const options = {
55
+ useGrouping: false,
56
+ minimumFractionDigits: digits ?? 0,
57
+ maximumFractionDigits: digits ?? 20,
58
+ };
59
+ return new Intl.NumberFormat(locale, options).format(value);
60
+ }
61
+ /**
62
+ * Parses locale-formatted text into a stored value. Group separators are
63
+ * display-only and never become part of the number. Invalid or mixed input
64
+ * returns `{ ok: false }` without echoing the raw text or an error message.
65
+ */
66
+ export function parseNumericInput(locale, text) {
67
+ const trimmed = text.trim();
68
+ if (trimmed === '')
69
+ return { ok: true, value: null };
70
+ if (trimmed.length > MAX_INPUT_LENGTH)
71
+ return { ok: false };
72
+ const { group, decimal } = separatorsFor(locale);
73
+ let index = 0;
74
+ let negative = false;
75
+ if (trimmed.startsWith('-') || trimmed.startsWith('\u2212')) {
76
+ negative = true;
77
+ index = 1;
78
+ }
79
+ if (index >= trimmed.length)
80
+ return { ok: false };
81
+ let integerDigits = '';
82
+ let fractionDigits = '';
83
+ let afterDecimal = false;
84
+ let sawDigit = false;
85
+ let groupCount = 0;
86
+ let firstGroupLength = 0;
87
+ let digitsSinceGroup = 0;
88
+ while (index < trimmed.length) {
89
+ if (decimal && trimmed.startsWith(decimal, index)) {
90
+ if (afterDecimal || groupCount > 0 && digitsSinceGroup !== 3)
91
+ return { ok: false };
92
+ afterDecimal = true;
93
+ index += decimal.length;
94
+ continue;
95
+ }
96
+ if (group && trimmed.startsWith(group, index)) {
97
+ if (afterDecimal || !sawDigit)
98
+ return { ok: false };
99
+ if (groupCount > 0 && digitsSinceGroup !== 3)
100
+ return { ok: false };
101
+ if (digitsSinceGroup < 1 || digitsSinceGroup > 3)
102
+ return { ok: false };
103
+ groupCount += 1;
104
+ if (groupCount === 1)
105
+ firstGroupLength = integerDigits.length;
106
+ digitsSinceGroup = 0;
107
+ index += group.length;
108
+ continue;
109
+ }
110
+ const char = trimmed[index] ?? '';
111
+ if (char < '0' || char > '9')
112
+ return { ok: false };
113
+ sawDigit = true;
114
+ if (afterDecimal) {
115
+ fractionDigits += char;
116
+ }
117
+ else {
118
+ integerDigits += char;
119
+ digitsSinceGroup += 1;
120
+ }
121
+ index += 1;
122
+ }
123
+ if (!sawDigit)
124
+ return { ok: false };
125
+ if (groupCount > 0) {
126
+ if (digitsSinceGroup !== 3 || firstGroupLength > 3)
127
+ return { ok: false };
128
+ }
129
+ if (afterDecimal && fractionDigits === '' && integerDigits === '')
130
+ return { ok: false };
131
+ const normalized = `${negative ? '-' : ''}${integerDigits || '0'}${afterDecimal ? `.${fractionDigits}` : ''}`;
132
+ const parsed = Number(normalized);
133
+ if (!Number.isFinite(parsed))
134
+ return { ok: false };
135
+ return { ok: true, value: Object.is(parsed, -0) ? 0 : parsed };
136
+ }
137
+ /** Rounds a stepped value to avoid floating-point drift when precision is set. */
138
+ export function roundToNumericPrecision(value, precision) {
139
+ const digits = clampNumericPrecision(precision);
140
+ if (digits === undefined)
141
+ return value;
142
+ const factor = 10 ** digits;
143
+ const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
144
+ return Object.is(rounded, -0) ? 0 : rounded;
145
+ }
146
+ /**
147
+ * Decimal scaling above 15 places would push 10^n past
148
+ * Number.MAX_SAFE_INTEGER, so the scaled path stops there. Larger decimal
149
+ * counts fall back to plain Number arithmetic, which keeps whatever the Number
150
+ * type can actually represent without promising sub-ulp precision.
151
+ */
152
+ const MAX_STEP_DECIMALS = 15;
153
+ function decimalPlaces(value) {
154
+ if (!Number.isFinite(value))
155
+ return 0;
156
+ const text = value.toString().toLowerCase();
157
+ const exponentIndex = text.indexOf('e');
158
+ if (exponentIndex >= 0) {
159
+ const mantissa = text.slice(0, exponentIndex);
160
+ const exponent = Number(text.slice(exponentIndex + 1));
161
+ const fractionLength = mantissa.split('.')[1]?.length ?? 0;
162
+ return Math.max(0, fractionLength - exponent);
163
+ }
164
+ return text.split('.')[1]?.length ?? 0;
165
+ }
166
+ /**
167
+ * Adds one step without rounding to the display precision. Display precision
168
+ * must not erase an explicit smaller step; decimal scaling only protects the
169
+ * arithmetic itself. Scaling is used while its integers stay inside
170
+ * Number.MAX_SAFE_INTEGER; beyond that, direct Number addition is the only
171
+ * truthful result. Non-finite results keep the original value.
172
+ */
173
+ export function addNumericStep(base, step, direction = 1) {
174
+ if (!Number.isFinite(base) || !Number.isFinite(step) || step <= 0)
175
+ return base;
176
+ const delta = direction * step;
177
+ const direct = base + delta;
178
+ if (!Number.isFinite(direct))
179
+ return base;
180
+ const decimals = Math.max(decimalPlaces(base), decimalPlaces(step));
181
+ if (decimals <= MAX_STEP_DECIMALS) {
182
+ const factor = 10 ** decimals;
183
+ const scaledBase = base * factor;
184
+ const scaledDelta = delta * factor;
185
+ if (Number.isFinite(scaledBase)
186
+ && Number.isFinite(scaledDelta)
187
+ && Math.abs(scaledBase) <= Number.MAX_SAFE_INTEGER
188
+ && Math.abs(scaledDelta) <= Number.MAX_SAFE_INTEGER) {
189
+ const scaled = Math.round(scaledBase) + Math.round(scaledDelta);
190
+ if (Math.abs(scaled) <= Number.MAX_SAFE_INTEGER) {
191
+ const next = scaled / factor;
192
+ if (Number.isFinite(next))
193
+ return Object.is(next, -0) ? 0 : next;
194
+ }
195
+ }
196
+ }
197
+ return Object.is(direct, -0) ? 0 : direct;
198
+ }
@@ -15,6 +15,8 @@ export function useRecordDetail(options) {
15
15
  void rendering;
16
16
  return {
17
17
  resource: binding.resource, id: options().id, dataProviderName: binding.dataProviderName,
18
+ // 详情页和抽屉已提供错误态与重试入口,避免重复通知。
19
+ errorNotification: false,
18
20
  queryOptions: { enabled: canRead },
19
21
  };
20
22
  });
@@ -25,6 +25,13 @@ export interface SavedListViewAccess {
25
25
  mode: SavedListViewAccessMode;
26
26
  subjectIds: string[];
27
27
  }
28
+ /** 解码时被裁剪的失效字段;只用于提示,不改变持久化数据。 */
29
+ export interface SavedListViewDecodeReport {
30
+ /** 所有视图裁剪列的并集,供聚合提示使用。 */
31
+ prunedColumnIds: string[];
32
+ /** 按视图 id 记录各自被裁剪的列,删除或覆盖单个视图时可精确清除。 */
33
+ prunedColumnsByView: Record<string, string[]>;
34
+ }
28
35
  export interface SavedListViewSubject {
29
36
  id: string;
30
37
  label: string;
@@ -87,13 +94,13 @@ export interface ListPreferenceScope {
87
94
  }
88
95
  export declare function decodeSavedListViewAccess(value: unknown): SavedListViewAccess | undefined;
89
96
  export declare function decodeSavedListViewSubjects(value: unknown): SavedListViewSubject[] | undefined;
90
- export declare function readSavedListViews(raw: string | null, allowedColumnIds: Set<string>, source?: SavedListView['source']): SavedListView[];
97
+ export declare function readSavedListViews(raw: string | null, allowedColumnIds: Set<string>, source?: SavedListView['source'], report?: SavedListViewDecodeReport): SavedListView[];
91
98
  export declare function decodeSavedListViewMutationResult(value: unknown, allowedColumnIds: Set<string>, expected?: Pick<SavedListViewMutation, 'id' | 'source' | 'expectedVersion'>): SavedListViewMutationSuccess | SavedListViewMutationConflict | undefined;
92
99
  export declare function decodeSavedListViewRemoveResult(value: unknown, expected: {
93
100
  id: string;
94
101
  expectedVersion: number;
95
102
  }): SavedListViewRemoveSuccess | undefined;
96
- export declare function decodeRemoteSavedListViews(value: unknown, allowedColumnIds: Set<string>, writable?: boolean): SavedListView[];
103
+ export declare function decodeRemoteSavedListViews(value: unknown, allowedColumnIds: Set<string>, writable?: boolean, report?: SavedListViewDecodeReport): SavedListView[];
97
104
  export declare function serializeSavedListViews(views: SavedListView[]): string;
98
105
  export declare function cloneSavedListViewState(state: SavedListViewState): SavedListViewState;
99
106
  export declare function listPreferenceScopeId(scope: ListPreferenceScope): string;
@@ -44,23 +44,41 @@ function parseSorters(value, allowedFieldIds) {
44
44
  }
45
45
  return sorters;
46
46
  }
47
- function parseColumnVisibility(value, allowedColumnIds) {
47
+ function parseColumnVisibility(value, allowedColumnIds, pruned) {
48
48
  if (!isRecord(value))
49
49
  return undefined;
50
50
  const visibility = {};
51
51
  for (const [columnId, visible] of Object.entries(value)) {
52
- if (!allowedColumnIds.has(columnId) || typeof visible !== 'boolean')
52
+ if (!allowedColumnIds.has(columnId)) {
53
+ pruned.add(columnId);
54
+ continue;
55
+ }
56
+ if (typeof visible !== 'boolean')
53
57
  continue;
54
58
  visibility[columnId] = visible;
55
59
  }
56
60
  return visibility;
57
61
  }
58
- function parseColumnOrder(value, allowedColumnIds) {
62
+ function parseColumnOrder(value, allowedColumnIds, pruned) {
59
63
  if (!Array.isArray(value))
60
64
  return undefined;
61
- return [...new Set(value.filter((columnId) => (typeof columnId === 'string' && allowedColumnIds.has(columnId))))];
65
+ const order = [];
66
+ const seen = new Set();
67
+ for (const columnId of value) {
68
+ if (typeof columnId !== 'string')
69
+ continue;
70
+ if (!allowedColumnIds.has(columnId)) {
71
+ pruned.add(columnId);
72
+ continue;
73
+ }
74
+ if (seen.has(columnId))
75
+ continue;
76
+ seen.add(columnId);
77
+ order.push(columnId);
78
+ }
79
+ return order;
62
80
  }
63
- function parseState(value, allowedColumnIds) {
81
+ function parseState(value, allowedColumnIds, pruned) {
64
82
  if (!isRecord(value))
65
83
  return undefined;
66
84
  if (typeof value['search'] !== 'string' || value['search'].length > 1_000)
@@ -76,8 +94,8 @@ function parseState(value, allowedColumnIds) {
76
94
  if (filters.some((entry) => entry === undefined))
77
95
  return undefined;
78
96
  const sorters = parseSorters(value['sorters'], allowedFieldIds);
79
- const columnVisibility = parseColumnVisibility(value['columnVisibility'], allowedColumnIds);
80
- const columnOrder = parseColumnOrder(value['columnOrder'], allowedColumnIds);
97
+ const columnVisibility = parseColumnVisibility(value['columnVisibility'], allowedColumnIds, pruned);
98
+ const columnOrder = parseColumnOrder(value['columnOrder'], allowedColumnIds, pruned);
81
99
  if (!sorters || !columnVisibility || !columnOrder)
82
100
  return undefined;
83
101
  return {
@@ -148,7 +166,7 @@ export function decodeSavedListViewSubjects(value) {
148
166
  return;
149
167
  }
150
168
  }
151
- export function readSavedListViews(raw, allowedColumnIds, source = 'local') {
169
+ export function readSavedListViews(raw, allowedColumnIds, source = 'local', report) {
152
170
  if (!raw)
153
171
  return [];
154
172
  try {
@@ -164,12 +182,25 @@ export function readSavedListViews(raw, allowedColumnIds, source = 'local') {
164
182
  continue;
165
183
  if (typeof entry['name'] !== 'string' || !entry['name'].trim() || entry['name'].length > 60)
166
184
  continue;
167
- const state = parseState(entry['state'], allowedColumnIds);
185
+ const pruned = new Set();
186
+ const state = parseState(entry['state'], allowedColumnIds, pruned);
168
187
  if (!state)
169
188
  continue;
170
189
  const access = entry['access'] === undefined ? undefined : parseAccess(entry['access']);
171
190
  if (entry['access'] !== undefined && !access)
172
191
  continue;
192
+ if (report) {
193
+ if (pruned.size > 0) {
194
+ // 视图 id 允许 "__proto__" 等特殊字符串,必须写为自身数据属性,避免触发原型 setter。
195
+ Object.defineProperty(report.prunedColumnsByView, entry['id'], {
196
+ value: [...pruned], enumerable: true, configurable: true, writable: true,
197
+ });
198
+ }
199
+ for (const columnId of pruned) {
200
+ if (!report.prunedColumnIds.includes(columnId))
201
+ report.prunedColumnIds.push(columnId);
202
+ }
203
+ }
173
204
  seenIds.add(entry['id']);
174
205
  views.push({
175
206
  id: entry['id'], name: entry['name'].trim(), state,
@@ -289,12 +320,12 @@ export function decodeSavedListViewRemoveResult(value, expected) {
289
320
  return;
290
321
  }
291
322
  }
292
- export function decodeRemoteSavedListViews(value, allowedColumnIds, writable = false) {
323
+ export function decodeRemoteSavedListViews(value, allowedColumnIds, writable = false, report) {
293
324
  try {
294
325
  const snapshot = snapshotPlainData(value);
295
326
  if (!Array.isArray(snapshot))
296
327
  return [];
297
- return readSavedListViews(JSON.stringify({ version: STORAGE_VERSION, views: snapshot }), allowedColumnIds, 'team')
328
+ return readSavedListViews(JSON.stringify({ version: STORAGE_VERSION, views: snapshot }), allowedColumnIds, 'team', report)
298
329
  .map(view => {
299
330
  const entry = snapshot.find(entry => isRecord(entry) && entry['id'] === view.id);
300
331
  return {
@@ -7,6 +7,8 @@ interface SavedListViewsOptions {
7
7
  isScopeLoaded: () => boolean;
8
8
  readPreference: (scope: ListPreferenceScope, key: string, legacyKey: string) => string | null;
9
9
  hasExplicitURLState: boolean;
10
+ /** 初始查询快照已优先于持久化视图时,不把旧活动视图标记为当前视图;默认 false。 */
11
+ initialStateOverridesSavedView?: boolean;
10
12
  canApplyRemoteDefault: () => boolean;
11
13
  captureState: () => SavedListViewState;
12
14
  applyState: (state: SavedListViewState) => void;
@@ -21,6 +23,7 @@ export declare function createSavedListViews(options: SavedListViewsOptions): {
21
23
  savedViewSource: "local" | "team" | "system";
22
24
  readonly remoteViewsLoading: boolean;
23
25
  readonly remoteViewsFailed: boolean;
26
+ readonly staleSavedViewColumns: string[];
24
27
  readonly savedViewMutationPending: boolean;
25
28
  readonly savedViewMutationError: "failure" | "conflict" | undefined;
26
29
  accessDrafts: SvelteMap<string, SavedListViewAccess>;
@@ -7,19 +7,25 @@ export function createSavedListViews(options) {
7
7
  const savedViewProvider = $derived(options.provider());
8
8
  const preferenceScopeIsLoaded = options.isScopeLoaded;
9
9
  const hasExplicitURLState = options.hasExplicitURLState;
10
- function readPreferences(scope) {
11
- const savedViews = readSavedListViews(options.readPreference(scope, savedListViewsStorageKey(scope), legacySavedListViewsStorageKey(scope.resourceName)), savedViewColumnIds);
10
+ function readPreferences(scope, report) {
11
+ report ??= { prunedColumnIds: [], prunedColumnsByView: {} };
12
+ const savedViews = readSavedListViews(options.readPreference(scope, savedListViewsStorageKey(scope), legacySavedListViewsStorageKey(scope.resourceName)), savedViewColumnIds, 'local', report);
12
13
  const candidate = options.readPreference(scope, activeSavedListViewStorageKey(scope), legacyActiveSavedListViewStorageKey(scope.resourceName));
13
14
  const activeSavedView = savedViews.find(view => view.id === candidate);
14
- return { savedViews, activeSavedView };
15
+ return { savedViews, activeSavedView, prunedColumnIds: report.prunedColumnIds };
15
16
  }
16
- const initial = untrack(() => readPreferences(listPreferenceScope));
17
- const initialSavedView = hasExplicitURLState ? undefined : initial.activeSavedView;
17
+ const initialReport = { prunedColumnIds: [], prunedColumnsByView: {} };
18
+ const initial = untrack(() => readPreferences(listPreferenceScope, initialReport));
19
+ const initialSavedView = hasExplicitURLState || (options.initialStateOverridesSavedView ?? false)
20
+ ? undefined
21
+ : initial.activeSavedView;
18
22
  let savedViews = $state(initial.savedViews);
19
23
  let activeSavedViewId = $state(initialSavedView?.id);
20
24
  let savedViewName = $state('');
21
25
  let savedViewsOpen = $state(false);
22
26
  let savedViewSource = $state('local');
27
+ let localStaleColumns = $state({ ...initialReport.prunedColumnsByView });
28
+ let remoteStaleColumns = $state({});
23
29
  let remoteSavedViews = $state([]);
24
30
  let remoteViewsLoading = $state(false);
25
31
  let remoteViewsFailed = $state(false);
@@ -45,6 +51,16 @@ export function createSavedListViews(options) {
45
51
  });
46
52
  });
47
53
  const activeSavedViewName = $derived(availableSavedViews.find(view => view.id === activeSavedViewId)?.name);
54
+ function clearStaleColumns(key, viewId) {
55
+ const current = key === 'local' ? localStaleColumns : remoteStaleColumns;
56
+ if (!(viewId in current))
57
+ return;
58
+ const next = Object.fromEntries(Object.entries(current).filter(([id]) => id !== viewId));
59
+ if (key === 'local')
60
+ localStaleColumns = next;
61
+ else
62
+ remoteStaleColumns = next;
63
+ }
48
64
  function persistSavedViews() {
49
65
  if (typeof window === 'undefined' || !preferenceScopeIsLoaded())
50
66
  return;
@@ -86,11 +102,14 @@ export function createSavedListViews(options) {
86
102
  // 表格完成列偏好和查询恢复后才打开持久化门,避免把旧租户状态写入新作用域。
87
103
  function restoreScope(scope) {
88
104
  remoteDefaultEligible = true;
89
- const preferences = readPreferences(scope);
105
+ const report = { prunedColumnIds: [], prunedColumnsByView: {} };
106
+ const preferences = readPreferences(scope, report);
90
107
  savedViewName = '';
91
108
  savedViewsOpen = false;
92
109
  savedViews = preferences.savedViews;
93
110
  remoteSavedViews = [];
111
+ localStaleColumns = { ...report.prunedColumnsByView };
112
+ remoteStaleColumns = {};
94
113
  activeSavedViewId = preferences.activeSavedView?.id;
95
114
  return preferences.activeSavedView;
96
115
  }
@@ -100,6 +119,7 @@ export function createSavedListViews(options) {
100
119
  const columns = savedViewColumnIds;
101
120
  void remoteViewsReload;
102
121
  remoteSavedViews = [];
122
+ remoteStaleColumns = {};
103
123
  remoteViewsLoading = false;
104
124
  remoteViewsFailed = false;
105
125
  accessDrafts.clear();
@@ -119,7 +139,9 @@ export function createSavedListViews(options) {
119
139
  void Promise.resolve().then(() => provider.list({ ...scope })).then(value => {
120
140
  if (epoch !== remoteViewEpoch || !preferenceScopeIsLoaded())
121
141
  return;
122
- remoteSavedViews = decodeRemoteSavedListViews(value, columns, !!(provider.save || provider.remove || provider.setDefault || provider.updateAccess));
142
+ const report = { prunedColumnIds: [], prunedColumnsByView: {} };
143
+ remoteSavedViews = decodeRemoteSavedListViews(value, columns, !!(provider.save || provider.remove || provider.setDefault || provider.updateAccess), report);
144
+ remoteStaleColumns = { ...report.prunedColumnsByView };
123
145
  if (options.canApplyRemoteDefault() && remoteDefaultEligible) {
124
146
  const defaults = remoteSavedViews.filter(view => view.default === true);
125
147
  if (!hasExplicitURLState && !activeSavedViewId && defaults.length === 1 && defaults[0]) {
@@ -169,6 +191,8 @@ export function createSavedListViews(options) {
169
191
  savedViews = existing
170
192
  ? savedViews.map(candidate => candidate.id === existing.id ? view : candidate)
171
193
  : [view, ...savedViews].slice(0, 25);
194
+ if (existing)
195
+ clearStaleColumns('local', existing.id);
172
196
  activeSavedViewId = view.id;
173
197
  savedViewName = '';
174
198
  persistSavedViews();
@@ -188,6 +212,7 @@ export function createSavedListViews(options) {
188
212
  return;
189
213
  }
190
214
  savedViews = savedViews.filter(view => view.id !== id);
215
+ clearStaleColumns('local', id);
191
216
  if (activeSavedViewId === id)
192
217
  activeSavedViewId = undefined;
193
218
  persistSavedViews();
@@ -217,6 +242,7 @@ export function createSavedListViews(options) {
217
242
  id: mutation.id, expectedVersion: mutation.expectedVersion,
218
243
  })) {
219
244
  remoteSavedViews = remoteSavedViews.filter(view => view.id !== mutation.id);
245
+ clearStaleColumns('remote', mutation.id);
220
246
  if (activeSavedViewId === mutation.id)
221
247
  activeSavedViewId = undefined;
222
248
  persistActiveSavedView();
@@ -234,6 +260,7 @@ export function createSavedListViews(options) {
234
260
  }
235
261
  const view = { ...decoded.view, readOnly: decoded.view.readOnly !== false };
236
262
  remoteSavedViews = [view, ...remoteSavedViews.filter(candidate => candidate.id !== view.id)];
263
+ clearStaleColumns('remote', view.id);
237
264
  savedViewName = '';
238
265
  }
239
266
  catch {
@@ -395,6 +422,28 @@ export function createSavedListViews(options) {
395
422
  set savedViewSource(value) { savedViewSource = value; },
396
423
  get remoteViewsLoading() { return remoteViewsLoading; },
397
424
  get remoteViewsFailed() { return remoteViewsFailed; },
425
+ get staleSavedViewColumns() {
426
+ const visibleLocal = new Set();
427
+ const visibleRemote = new Set();
428
+ for (const view of availableSavedViews) {
429
+ if (view.source === 'team' || view.source === 'system')
430
+ visibleRemote.add(view.id);
431
+ else
432
+ visibleLocal.add(view.id);
433
+ }
434
+ const columns = new Set();
435
+ for (const [viewId, pruned] of Object.entries(localStaleColumns)) {
436
+ if (visibleLocal.has(viewId))
437
+ for (const columnId of pruned)
438
+ columns.add(columnId);
439
+ }
440
+ for (const [viewId, pruned] of Object.entries(remoteStaleColumns)) {
441
+ if (visibleRemote.has(viewId))
442
+ for (const columnId of pruned)
443
+ columns.add(columnId);
444
+ }
445
+ return [...columns];
446
+ },
398
447
  get savedViewMutationPending() { return savedViewMutationPending; },
399
448
  get savedViewMutationError() { return savedViewMutationError; },
400
449
  accessDrafts, accessSubjects, accessSubjectsLoading, accessSubjectsFailed, accessSubjectQueries,
@@ -10,4 +10,5 @@ export declare function createTaskAction(options: {
10
10
  run: () => Promise<void>;
11
11
  readonly pending: boolean;
12
12
  readonly available: boolean;
13
+ readonly failed: boolean;
13
14
  };
@@ -1,6 +1,27 @@
1
1
  import { onDestroy } from 'svelte';
2
+ import { SvelteMap } from 'svelte/reactivity';
2
3
  import { captureAdminContext, captureAuthSession, queryKeyMatches, TaskError, withValidatedTaskProvider } from '@svadmin/core';
3
4
  import { useQueryClient } from '@tanstack/svelte-query';
5
+ // 同一任务的重试与取消共享一个在途声明,避免不同按钮或重复挂载同时写入。
6
+ let nextProviderId = 1;
7
+ const providerIds = new WeakMap();
8
+ const actionClaims = new SvelteMap();
9
+ function claimKey(scope) {
10
+ if (!scope.provider)
11
+ return undefined;
12
+ let providerId = providerIds.get(scope.provider);
13
+ if (providerId === undefined) {
14
+ providerId = nextProviderId++;
15
+ providerIds.set(scope.provider, providerId);
16
+ }
17
+ return JSON.stringify([providerId, scope.id, scope.tenant ?? null, scope.session.cacheKey]);
18
+ }
19
+ function sameActionScope(left, right) {
20
+ return left.provider === right.provider && left.id === right.id
21
+ && left.tenant === right.tenant && left.auth === right.auth
22
+ && left.router === right.router && left.access === right.access
23
+ && left.session.cacheKey === right.session.cacheKey && left.session.isCurrent();
24
+ }
4
25
  export function createTaskAction(options) {
5
26
  const context = captureAdminContext();
6
27
  const client = useQueryClient();
@@ -12,28 +33,50 @@ export function createTaskAction(options) {
12
33
  });
13
34
  let mounted = true;
14
35
  let attempt = $state.raw();
36
+ let failureScope = $state.raw();
15
37
  function current(origin) {
16
38
  return mounted && origin.provider === scope.provider && origin.id === scope.id
17
39
  && origin.tenant === scope.tenant && origin.auth === scope.auth
18
40
  && origin.router === scope.router && origin.access === scope.access
19
41
  && origin.session.isCurrent();
20
42
  }
21
- onDestroy(() => { mounted = false; attempt = undefined; });
43
+ onDestroy(() => {
44
+ mounted = false;
45
+ // 卸载只停止结果交付;在途声明由请求结算释放,避免按钮切换重开并发窗口。
46
+ attempt = undefined;
47
+ });
22
48
  $effect.pre(() => {
23
49
  if (attempt && !current(attempt.scope))
24
50
  attempt = undefined;
51
+ if (failureScope && !current(failureScope))
52
+ failureScope = undefined;
25
53
  });
26
- const pending = $derived(!!attempt && current(attempt.scope));
54
+ const claim = $derived.by(() => {
55
+ const key = claimKey(scope);
56
+ if (!key)
57
+ return undefined;
58
+ const currentClaim = actionClaims.get(key);
59
+ return currentClaim && sameActionScope(currentClaim.scope, scope) ? currentClaim : undefined;
60
+ });
61
+ const pending = $derived(!!claim);
27
62
  const available = $derived(!options.disabled && scope.session.available && !!provider?.[options.action]);
28
63
  async function run() {
29
64
  if (!available || pending || !mounted || !provider)
30
65
  return;
31
66
  const origin = scope;
67
+ const key = claimKey(origin);
68
+ if (!key)
69
+ return;
70
+ const existing = actionClaims.get(key);
71
+ if (existing && sameActionScope(existing.scope, origin))
72
+ return;
32
73
  const token = { scope: origin };
33
74
  const matcher = context.queryKeyMatcher();
34
75
  const success = options.onSuccess;
35
76
  const failure = options.onError;
77
+ failureScope = undefined;
36
78
  attempt = token;
79
+ actionClaims.set(key, token);
37
80
  let error;
38
81
  let failed = false;
39
82
  let deliver;
@@ -57,9 +100,13 @@ export function createTaskAction(options) {
57
100
  deliver = current(origin) && attempt === token;
58
101
  if (attempt === token)
59
102
  attempt = undefined;
103
+ if (actionClaims.get(key) === token)
104
+ actionClaims.delete(key);
60
105
  }
61
106
  if (!deliver || !current(origin))
62
107
  return;
108
+ if (failed)
109
+ failureScope = origin;
63
110
  // 观察者异常不能被误报为服务端操作失败,也不能形成未处理的 Promise。
64
111
  try {
65
112
  if (failed)
@@ -69,5 +116,10 @@ export function createTaskAction(options) {
69
116
  }
70
117
  catch { /* 回调由宿主管理,不重试已发出的写入。 */ }
71
118
  }
72
- return { run, get pending() { return pending; }, get available() { return available; } };
119
+ return {
120
+ run,
121
+ get pending() { return pending; },
122
+ get available() { return available; },
123
+ get failed() { return !!failureScope && current(failureScope); },
124
+ };
73
125
  }
@@ -1,4 +1,5 @@
1
1
  import type { TaskRecord } from '@svadmin/core';
2
+ export declare function formatTaskDate(value: unknown, locale: string): string;
2
3
  export type TaskLifecycle = 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'unknown';
3
4
  export declare function normalizeTaskStatus(status: unknown): TaskLifecycle;
4
5
  export declare function isTaskActive(task: TaskRecord): boolean;
@@ -1,3 +1,21 @@
1
+ import { parseDisplayDate } from './fields/date-display';
2
+ export function formatTaskDate(value, locale) {
3
+ if (typeof value !== 'string' || !value)
4
+ return '\u2014';
5
+ const parsed = parseDisplayDate(value);
6
+ if (!parsed)
7
+ return '\u2014';
8
+ try {
9
+ return new Intl.DateTimeFormat(locale, {
10
+ dateStyle: 'medium',
11
+ timeStyle: 'medium',
12
+ ...(parsed.civil ? { timeZone: 'UTC' } : {}),
13
+ }).format(parsed.date);
14
+ }
15
+ catch {
16
+ return parsed.title;
17
+ }
18
+ }
1
19
  const STATUS_ALIASES = {
2
20
  pending: 'queued',
3
21
  queued: 'queued',
@@ -14,7 +14,7 @@
14
14
  type Props = WithElementRef<Omit<HTMLInputAttributes, "type" | "value" | "size">> &
15
15
  { size?: "default" | "compact"; nativeSize?: number } &
16
16
  (Type extends "file"
17
- ? { type: "file"; files?: FileList; value?: never }
17
+ ? { type: "file"; files?: FileList | undefined; value?: never }
18
18
  : { type?: Type; files?: undefined; value?: InputValue });
19
19
 
20
20
  let {
@@ -7,7 +7,7 @@ declare function $$render<Type extends HTMLInputTypeAttribute | undefined = unde
7
7
  nativeSize?: number;
8
8
  } & (Type extends "file" ? {
9
9
  type: "file";
10
- files?: FileList;
10
+ files?: FileList | undefined;
11
11
  value?: never;
12
12
  } : {
13
13
  type?: Type;