@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.
- package/dist/app.css +4 -0
- package/dist/app.theme.css +4 -0
- package/dist/component-registry.svelte.d.ts +2 -2
- package/dist/components/AboutSettings.svelte +23 -4
- package/dist/components/AdminApp.svelte +6 -4
- package/dist/components/AdminApp.svelte.d.ts +3 -3
- package/dist/components/ApiSettings.svelte +43 -27
- package/dist/components/ApprovalActionCard.svelte +8 -9
- package/dist/components/ApprovalCenter.svelte +24 -5
- package/dist/components/AuditLogViewer.svelte +2 -2
- package/dist/components/AutoComplete.svelte +610 -0
- package/dist/components/AutoComplete.svelte.d.ts +25 -0
- package/dist/components/AutoForm.svelte +265 -18
- package/dist/components/AutoForm.svelte.d.ts +19 -2
- package/dist/components/AutoTable.svelte +141 -36
- package/dist/components/AutoTable.svelte.d.ts +8 -0
- package/dist/components/BatchActionBar.svelte +42 -14
- package/dist/components/BatchActionBar.svelte.d.ts +5 -0
- package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
- package/dist/components/CancelTaskButton.svelte +14 -1
- package/dist/components/Cascader.svelte +412 -53
- package/dist/components/Cascader.svelte.d.ts +7 -1
- package/dist/components/ColorPicker.svelte +1 -1
- package/dist/components/ColorPicker.svelte.d.ts +1 -1
- package/dist/components/ComboboxField.svelte +270 -34
- package/dist/components/ComboboxField.svelte.d.ts +4 -0
- package/dist/components/CommandPalette.svelte +4 -4
- package/dist/components/DashboardView.svelte +73 -7
- package/dist/components/DashboardView.svelte.d.ts +3 -1
- package/dist/components/DateRangeInput.svelte +8 -1
- package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
- package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
- package/dist/components/DetailWorkspaceSections.svelte +233 -0
- package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
- package/dist/components/DrawerForm.svelte +78 -9
- package/dist/components/DrawerForm.svelte.d.ts +3 -3
- package/dist/components/ErrorBoundary.svelte +1 -1
- package/dist/components/ErrorComponent.svelte +19 -6
- package/dist/components/ErrorComponent.svelte.d.ts +1 -0
- package/dist/components/ErrorSummary.svelte +9 -4
- package/dist/components/FieldRenderer.svelte +203 -90
- package/dist/components/FieldRenderer.svelte.d.ts +16 -1
- package/dist/components/FileUpload.svelte +116 -51
- package/dist/components/FileUpload.svelte.d.ts +7 -27
- package/dist/components/FileUploadField.svelte +170 -0
- package/dist/components/FileUploadField.svelte.d.ts +21 -0
- package/dist/components/FilterBuilder.svelte +13 -7
- package/dist/components/FilterCollectionInput.svelte +9 -7
- package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
- package/dist/components/ForgotPasswordPage.svelte +1 -1
- package/dist/components/Header.svelte +1 -1
- package/dist/components/ImportWizard.svelte +197 -28
- package/dist/components/ImportWizard.svelte.d.ts +5 -1
- package/dist/components/InferencerPanel.svelte +4 -2
- package/dist/components/InfiniteList.svelte +52 -9
- package/dist/components/JsonSchemaForm.svelte +237 -42
- package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
- package/dist/components/KanbanBoard.svelte +43 -7
- package/dist/components/KanbanBoard.svelte.d.ts +2 -2
- package/dist/components/Layout.svelte +1 -1
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/MetricStrip.svelte +8 -3
- package/dist/components/MetricStrip.svelte.d.ts +1 -0
- package/dist/components/ModalForm.svelte +85 -9
- package/dist/components/ModalForm.svelte.d.ts +3 -3
- package/dist/components/MoneyInput.svelte +5 -3
- package/dist/components/MoneyInput.svelte.d.ts +4 -2
- package/dist/components/MultiTabKeepAlive.svelte +502 -43
- package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
- package/dist/components/NotificationsSettings.svelte +10 -10
- package/dist/components/NumberInput.svelte +5 -3
- package/dist/components/NumberInput.svelte.d.ts +4 -2
- package/dist/components/NumericInput.svelte +277 -25
- package/dist/components/NumericInput.svelte.d.ts +3 -1
- package/dist/components/PercentInput.svelte +5 -3
- package/dist/components/PercentInput.svelte.d.ts +4 -2
- package/dist/components/PermissionMatrix.svelte +44 -8
- package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
- package/dist/components/ProfilePage.svelte +38 -13
- package/dist/components/RadioGroup.svelte +203 -4
- package/dist/components/RadioGroup.svelte.d.ts +1 -1
- package/dist/components/Rate.svelte +3 -3
- package/dist/components/Rate.svelte.d.ts +2 -2
- package/dist/components/RecordDetailDrawer.svelte +1 -1
- package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
- package/dist/components/RegisterPage.svelte +1 -1
- package/dist/components/RelationPicker.svelte +39 -11
- package/dist/components/ResourceAccessGuard.svelte +7 -2
- package/dist/components/RetryTaskButton.svelte +14 -1
- package/dist/components/RolesSettings.svelte +74 -28
- package/dist/components/SecuritySettings.svelte +66 -51
- package/dist/components/SegmentedControl.svelte +4 -2
- package/dist/components/SegmentedControl.svelte.d.ts +1 -1
- package/dist/components/Sidebar.svelte +1 -1
- package/dist/components/StepsForm.svelte +62 -10
- package/dist/components/StepsForm.svelte.d.ts +4 -2
- package/dist/components/SvadminLogo.svelte +1 -0
- package/dist/components/SvarDataGrid.svelte +42 -24
- package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
- package/dist/components/SvarResourceRead.svelte +2 -0
- package/dist/components/SvarResourceTableView.svelte +4 -2
- package/dist/components/TagsInput.svelte +3 -3
- package/dist/components/TagsInput.svelte.d.ts +2 -2
- package/dist/components/TaskDetails.svelte +22 -16
- package/dist/components/TaskDetails.svelte.d.ts +1 -1
- package/dist/components/TaskList.svelte +57 -15
- package/dist/components/TaskQueueDrawer.svelte +54 -34
- package/dist/components/Toast.svelte +13 -2
- package/dist/components/Transfer.svelte +67 -5
- package/dist/components/Transfer.svelte.d.ts +4 -0
- package/dist/components/TreeSelect.svelte +337 -77
- package/dist/components/TreeSelect.svelte.d.ts +4 -0
- package/dist/components/TreeTable.svelte +6 -1
- package/dist/components/TwoFactorAuthPage.svelte +8 -4
- package/dist/components/UpdatePasswordPage.svelte +1 -1
- package/dist/components/VersionDiffViewer.svelte +9 -7
- package/dist/components/account/CompanyProfilePage.svelte +13 -12
- package/dist/components/account/MemberDirectory.svelte +14 -10
- package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
- package/dist/components/content/AuditTimeline.svelte +8 -6
- package/dist/components/content/DataState.svelte +1 -1
- package/dist/components/content/FileList.svelte +44 -13
- package/dist/components/content/FileList.svelte.d.ts +4 -2
- package/dist/components/content/FileListRow.svelte +87 -0
- package/dist/components/content/FileListRow.svelte.d.ts +11 -0
- package/dist/components/content/ImportDropzone.svelte +109 -12
- package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
- package/dist/components/content/IntegrationCard.svelte +3 -3
- package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
- package/dist/components/content/MediaThumbnail.svelte +6 -2
- package/dist/components/detail-workspace.d.ts +60 -0
- package/dist/components/detail-workspace.js +3 -0
- package/dist/components/enterprise/json-schema-form.d.ts +1 -1
- package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
- package/dist/components/enterprise/recursive-schema-form.js +288 -14
- package/dist/components/enterprise/schema-form-references.d.ts +14 -0
- package/dist/components/enterprise/schema-form-references.js +316 -0
- package/dist/components/fields/ArrayField.svelte +178 -14
- package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
- package/dist/components/fields/array-field-metadata.d.ts +4 -0
- package/dist/components/fields/array-field-metadata.js +36 -0
- package/dist/components/file-upload-reference.d.ts +11 -0
- package/dist/components/file-upload-reference.js +50 -0
- package/dist/components/file-upload-types.d.ts +32 -0
- package/dist/components/file-upload-types.js +1 -0
- package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
- package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
- package/dist/components/json-schema-form-state.d.ts +19 -0
- package/dist/components/list-deletion.svelte.js +6 -1
- package/dist/components/list-state.svelte.d.ts +12 -1
- package/dist/components/list-state.svelte.js +56 -16
- package/dist/components/numeric-format.d.ts +44 -0
- package/dist/components/numeric-format.js +198 -0
- package/dist/components/record-detail.svelte.js +2 -0
- package/dist/components/saved-list-views.d.ts +9 -2
- package/dist/components/saved-list-views.js +42 -11
- package/dist/components/saved-list-views.svelte.d.ts +3 -0
- package/dist/components/saved-list-views.svelte.js +56 -7
- package/dist/components/task-action.svelte.d.ts +1 -0
- package/dist/components/task-action.svelte.js +55 -3
- package/dist/components/task-utils.d.ts +1 -0
- package/dist/components/task-utils.js +18 -0
- package/dist/components/ui/input/input.svelte +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/sheet/sheet.svelte +4 -1
- package/dist/components/version-history.d.ts +14 -0
- package/dist/components/version-history.js +32 -0
- package/dist/components/workspace-tab-state.d.ts +78 -0
- package/dist/components/workspace-tab-state.js +587 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +4 -0
- 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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
17
|
-
const
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -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(() => {
|
|
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
|
|
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 {
|
|
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 {
|