@svadmin/ui 0.72.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -25
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/app.css +7694 -5117
- package/dist/app.theme.css +7645 -5119
- package/dist/class-properties.generated.d.ts +1 -0
- package/dist/class-properties.generated.js +2 -0
- package/dist/classnames.d.ts +8 -0
- package/dist/classnames.js +26 -0
- package/dist/components/AutoForm.svelte +20 -6
- package/dist/components/AutoForm.svelte.d.ts +2 -0
- package/dist/components/AutoTable.svelte +249 -9
- package/dist/components/AutoTable.svelte.d.ts +5 -0
- package/dist/components/BoundRecordDetailDrawer.svelte +8 -2
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
- package/dist/components/ComboboxField.svelte +31 -9
- package/dist/components/ComboboxField.svelte.d.ts +3 -2
- package/dist/components/CreatePage.svelte +4 -0
- package/dist/components/CreatePage.svelte.d.ts +2 -0
- package/dist/components/DateRangeInput.svelte +75 -0
- package/dist/components/DateRangeInput.svelte.d.ts +25 -0
- package/dist/components/DateTimeInput.svelte +65 -0
- package/dist/components/DateTimeInput.svelte.d.ts +19 -0
- package/dist/components/DevTools.svelte +2 -2
- package/dist/components/EditPage.svelte +4 -0
- package/dist/components/EditPage.svelte.d.ts +2 -0
- package/dist/components/FieldRenderer.svelte +90 -18
- package/dist/components/FileUpload.svelte +250 -0
- package/dist/components/FileUpload.svelte.d.ts +32 -0
- package/dist/components/FilterBuilder.svelte +236 -279
- package/dist/components/FilterBuilder.svelte.d.ts +3 -0
- package/dist/components/ImportWizard.svelte +181 -94
- package/dist/components/JsonSchemaForm.svelte +255 -99
- package/dist/components/JsonSchemaForm.svelte.d.ts +12 -1
- package/dist/components/Layout.svelte +1 -2
- package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
- package/dist/components/ListPage.svelte +4 -0
- package/dist/components/ListPage.svelte.d.ts +2 -0
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/MoneyInput.svelte +25 -0
- package/dist/components/MoneyInput.svelte.d.ts +20 -0
- package/dist/components/NumberInput.svelte +25 -0
- package/dist/components/NumberInput.svelte.d.ts +19 -0
- package/dist/components/NumericInput.svelte +110 -0
- package/dist/components/NumericInput.svelte.d.ts +23 -0
- package/dist/components/PercentInput.svelte +25 -0
- package/dist/components/PercentInput.svelte.d.ts +20 -0
- package/dist/components/QuickEditDrawer.svelte +4 -1
- package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
- package/dist/components/RecordDetailDrawer.svelte +4 -2
- package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
- package/dist/components/ResourceOperationsPage.svelte +25 -19
- package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
- package/dist/components/ShowInferencer.svelte +4 -2
- package/dist/components/ShowInferencer.svelte.d.ts +2 -0
- package/dist/components/ShowPage.svelte +7 -3
- package/dist/components/ShowPage.svelte.d.ts +2 -0
- package/dist/components/Sidebar.svelte +5 -5
- package/dist/components/SpreadsheetView.svelte +81 -286
- package/dist/components/StepsForm.svelte +5 -5
- package/dist/components/SvarAutoTable.svelte +39 -0
- package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
- package/dist/components/SvarDataGrid.svelte +362 -0
- package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
- package/dist/components/SvarGridCell.svelte +10 -0
- package/dist/components/SvarGridCell.svelte.d.ts +7 -0
- package/dist/components/SvarResourceRead.svelte +50 -0
- package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
- package/dist/components/SvarResourceTable.svelte +63 -0
- package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
- package/dist/components/SvarResourceTableView.svelte +364 -0
- package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
- package/dist/components/TaskQueueDrawer.svelte +72 -16
- package/dist/components/TreeSelect.svelte +208 -28
- package/dist/components/TreeSelect.svelte.d.ts +3 -1
- package/dist/components/auto-table-grid.d.ts +26 -0
- package/dist/components/auto-table-grid.js +1 -0
- package/dist/components/content/ContentPageHeader.svelte +12 -9
- package/dist/components/content/ContentPageShell.svelte +6 -9
- package/dist/components/content/FileBrowser.svelte +35 -0
- package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
- package/dist/components/content/MetricBlock.svelte +10 -13
- package/dist/components/content/NetworkTable.svelte +3 -3
- package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
- package/dist/components/content/PageToolbar.svelte +6 -10
- package/dist/components/content/SectionHeader.svelte +8 -13
- package/dist/components/content/SettingsFieldRow.svelte +9 -23
- package/dist/components/content/SettingsGroup.svelte +11 -27
- package/dist/components/content/StatusBadge.svelte +3 -8
- package/dist/components/content/WorkspaceLayout.svelte +8 -12
- package/dist/components/enterprise/filter-tree.d.ts +48 -0
- package/dist/components/enterprise/filter-tree.js +224 -0
- package/dist/components/enterprise/json-schema-form.d.ts +39 -0
- package/dist/components/enterprise/json-schema-form.js +195 -0
- package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
- package/dist/components/enterprise/recursive-schema-form.js +196 -0
- package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
- package/dist/components/enterprise/spreadsheet-formula.js +245 -0
- package/dist/components/fieldComponentMap.js +5 -0
- package/dist/components/fields/DateField.svelte +4 -3
- package/dist/components/fields/date-field-value.d.ts +2 -0
- package/dist/components/fields/date-field-value.js +13 -0
- package/dist/components/json-schema-form-state.d.ts +37 -0
- package/dist/components/json-schema-form-state.js +168 -0
- package/dist/components/record-detail.svelte.d.ts +3 -0
- package/dist/components/record-detail.svelte.js +10 -4
- package/dist/components/svar-grid-cells.d.ts +14 -0
- package/dist/components/svar-grid-cells.js +2 -0
- package/dist/components/svar-grid-contract.d.ts +80 -0
- package/dist/components/svar-grid-contract.js +159 -0
- package/dist/components/svar-grid-interactions.d.ts +54 -0
- package/dist/components/svar-grid-interactions.js +174 -0
- package/dist/components/svar-grid-loading.d.ts +40 -0
- package/dist/components/svar-grid-loading.js +180 -0
- package/dist/components/svar-grid-operations.d.ts +37 -0
- package/dist/components/svar-grid-operations.js +186 -0
- package/dist/components/svar-grid-panes.d.ts +34 -0
- package/dist/components/svar-grid-panes.js +66 -0
- package/dist/components/svar-grid-resource-loading.d.ts +18 -0
- package/dist/components/svar-grid-resource-loading.js +68 -0
- package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
- package/dist/components/svar-grid-resource-session.svelte.js +61 -0
- package/dist/components/ui/badge/badge-variants.js +3 -1
- package/dist/components/ui/button/button-variants.js +5 -2
- package/dist/components/ui/input/input.svelte +1 -4
- package/dist/components/ui/textarea/textarea.svelte +2 -1
- package/dist/components.css +3079 -0
- package/dist/design-contract.d.ts +13 -0
- package/dist/design-contract.js +11 -0
- package/dist/design-system.d.ts +15 -0
- package/dist/design-system.js +7 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/page-chrome.css +12 -49
- package/dist/recipes.d.ts +4 -0
- package/dist/recipes.js +4 -0
- package/dist/rendering/index.d.ts +87 -0
- package/dist/rendering/index.js +117 -0
- package/dist/styled-system/css/conditions.js +36 -0
- package/dist/styled-system/css/css.d.ts +22 -0
- package/dist/styled-system/css/css.js +45 -0
- package/dist/styled-system/css/cva.d.ts +6 -0
- package/dist/styled-system/css/cva.js +87 -0
- package/dist/styled-system/css/cx.d.ts +5 -0
- package/dist/styled-system/css/cx.js +15 -0
- package/dist/styled-system/css/index.d.ts +5 -0
- package/dist/styled-system/css/index.js +4 -0
- package/dist/styled-system/css/sva.d.ts +4 -0
- package/dist/styled-system/css/sva.js +46 -0
- package/dist/styled-system/helpers.js +316 -0
- package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
- package/dist/styled-system/patterns/aspect-ratio.js +38 -0
- package/dist/styled-system/patterns/bleed.d.ts +21 -0
- package/dist/styled-system/patterns/bleed.js +24 -0
- package/dist/styled-system/patterns/box.d.ts +20 -0
- package/dist/styled-system/patterns/box.js +15 -0
- package/dist/styled-system/patterns/center.d.ts +20 -0
- package/dist/styled-system/patterns/center.js +21 -0
- package/dist/styled-system/patterns/circle.d.ts +20 -0
- package/dist/styled-system/patterns/circle.js +25 -0
- package/dist/styled-system/patterns/container.d.ts +20 -0
- package/dist/styled-system/patterns/container.js +21 -0
- package/dist/styled-system/patterns/cq.d.ts +21 -0
- package/dist/styled-system/patterns/cq.js +21 -0
- package/dist/styled-system/patterns/divider.d.ts +22 -0
- package/dist/styled-system/patterns/divider.js +25 -0
- package/dist/styled-system/patterns/flex.d.ts +26 -0
- package/dist/styled-system/patterns/flex.js +26 -0
- package/dist/styled-system/patterns/float.d.ts +23 -0
- package/dist/styled-system/patterns/float.js +52 -0
- package/dist/styled-system/patterns/grid-item.d.ts +25 -0
- package/dist/styled-system/patterns/grid-item.js +25 -0
- package/dist/styled-system/patterns/grid.d.ts +24 -0
- package/dist/styled-system/patterns/grid.js +27 -0
- package/dist/styled-system/patterns/hstack.d.ts +21 -0
- package/dist/styled-system/patterns/hstack.js +24 -0
- package/dist/styled-system/patterns/index.d.ts +21 -0
- package/dist/styled-system/patterns/index.js +20 -0
- package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
- package/dist/styled-system/patterns/link-overlay.js +24 -0
- package/dist/styled-system/patterns/spacer.d.ts +20 -0
- package/dist/styled-system/patterns/spacer.js +25 -0
- package/dist/styled-system/patterns/square.d.ts +20 -0
- package/dist/styled-system/patterns/square.js +24 -0
- package/dist/styled-system/patterns/stack.d.ts +23 -0
- package/dist/styled-system/patterns/stack.js +24 -0
- package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
- package/dist/styled-system/patterns/visually-hidden.js +18 -0
- package/dist/styled-system/patterns/vstack.d.ts +21 -0
- package/dist/styled-system/patterns/vstack.js +24 -0
- package/dist/styled-system/patterns/wrap.d.ts +24 -0
- package/dist/styled-system/patterns/wrap.js +25 -0
- package/dist/styled-system/recipes/content-header.d.ts +31 -0
- package/dist/styled-system/recipes/content-header.js +65 -0
- package/dist/styled-system/recipes/content-page.d.ts +34 -0
- package/dist/styled-system/recipes/content-page.js +43 -0
- package/dist/styled-system/recipes/create-recipe.js +82 -0
- package/dist/styled-system/recipes/index.d.ts +17 -0
- package/dist/styled-system/recipes/index.js +16 -0
- package/dist/styled-system/recipes/metric-block.d.ts +34 -0
- package/dist/styled-system/recipes/metric-block.js +76 -0
- package/dist/styled-system/recipes/product-list.d.ts +31 -0
- package/dist/styled-system/recipes/product-list.js +73 -0
- package/dist/styled-system/recipes/product-section.d.ts +31 -0
- package/dist/styled-system/recipes/product-section.js +49 -0
- package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
- package/dist/styled-system/recipes/product-settings-row.js +58 -0
- package/dist/styled-system/recipes/product-settings.d.ts +31 -0
- package/dist/styled-system/recipes/product-settings.js +57 -0
- package/dist/styled-system/recipes/product-status.d.ts +34 -0
- package/dist/styled-system/recipes/product-status.js +45 -0
- package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
- package/dist/styled-system/recipes/product-toolbar.js +41 -0
- package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
- package/dist/styled-system/recipes/product-workspace.js +58 -0
- package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
- package/dist/styled-system/recipes/surface-metric.js +63 -0
- package/dist/styled-system/recipes/surface-table.d.ts +34 -0
- package/dist/styled-system/recipes/surface-table.js +62 -0
- package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
- package/dist/styled-system/recipes/ui-badge.js +38 -0
- package/dist/styled-system/recipes/ui-button.d.ts +32 -0
- package/dist/styled-system/recipes/ui-button.js +43 -0
- package/dist/styled-system/recipes/ui-input.d.ts +31 -0
- package/dist/styled-system/recipes/ui-input.js +49 -0
- package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
- package/dist/styled-system/recipes/ui-textarea.js +24 -0
- package/dist/styled-system/styles.css +1257 -0
- package/dist/styled-system/tokens/index.d.ts +9 -0
- package/dist/styled-system/tokens/index.js +268 -0
- package/dist/styled-system/tokens/tokens.d.ts +36 -0
- package/dist/styled-system/types/composition.d.ts +227 -0
- package/dist/styled-system/types/conditions.d.ts +236 -0
- package/dist/styled-system/types/csstype.d.ts +22570 -0
- package/dist/styled-system/types/index.d.ts +6 -0
- package/dist/styled-system/types/parts.d.ts +8 -0
- package/dist/styled-system/types/pattern.d.ts +78 -0
- package/dist/styled-system/types/prop-type.d.ts +223 -0
- package/dist/styled-system/types/recipe.d.ts +181 -0
- package/dist/styled-system/types/selectors.d.ts +59 -0
- package/dist/styled-system/types/static-css.d.ts +56 -0
- package/dist/styled-system/types/style-props.d.ts +8088 -0
- package/dist/styled-system/types/system-types.d.ts +151 -0
- package/dist/styles/aliases.css +49 -0
- package/dist/styles/compatibility.epilogue.css +384 -0
- package/dist/styles/compatibility.prelude.css +5513 -0
- package/dist/styles/conditional-states.css +28 -0
- package/dist/styles/conditional-utilities.css +22 -0
- package/dist/styles/recipes.css +1262 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +31 -13
- package/styles-compatibility.json +12 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { checkedSvarColumns, buildSvarColumns, svarColumnId, nextSvarSort, nextSvarFilter, SVAR_BLOCKED_ACTIONS } from './svar-grid-contract.js';
|
|
2
|
+
import { svarRecordIndex, svarRecordKey } from './svar-grid-operations.js';
|
|
3
|
+
export function checkedSvarInteractiveColumns(columns) {
|
|
4
|
+
const checked = checkedSvarColumns(columns);
|
|
5
|
+
const formats = new Set(['plain', 'number', 'currency', 'percent', 'date', 'datetime', 'boolean']);
|
|
6
|
+
return checked.map((column, index) => {
|
|
7
|
+
const source = columns[index];
|
|
8
|
+
if (!source)
|
|
9
|
+
throw new Error('Missing column');
|
|
10
|
+
if (source.editable !== undefined && source.editable !== 'text' && source.editable !== 'number')
|
|
11
|
+
throw new Error('Invalid editor');
|
|
12
|
+
if (source.format !== undefined && !formats.has(source.format))
|
|
13
|
+
throw new Error('Invalid value format');
|
|
14
|
+
if (source.currency !== undefined && !/^[A-Z]{3}$/u.test(source.currency))
|
|
15
|
+
throw new Error('Invalid currency');
|
|
16
|
+
return { ...column,
|
|
17
|
+
...(source.editable === undefined ? {} : { editable: source.editable }),
|
|
18
|
+
...(source.format === undefined ? {} : { format: source.format }),
|
|
19
|
+
...(source.currency === undefined ? {} : { currency: source.currency }),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function formatSvarValue(value, column, locale) {
|
|
24
|
+
if (value == null)
|
|
25
|
+
return '—';
|
|
26
|
+
const format = column.format ?? 'plain';
|
|
27
|
+
if (format === 'boolean')
|
|
28
|
+
return value === true ? (locale.startsWith('zh') ? '是' : 'Yes') : value === false ? (locale.startsWith('zh') ? '否' : 'No') : '—';
|
|
29
|
+
if (format === 'number' || format === 'currency' || format === 'percent') {
|
|
30
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
31
|
+
return '—';
|
|
32
|
+
const options = format === 'currency'
|
|
33
|
+
? { style: 'currency', currency: column.currency ?? 'USD' }
|
|
34
|
+
: format === 'percent' ? { style: 'percent', maximumFractionDigits: 2 } : {};
|
|
35
|
+
return new Intl.NumberFormat(locale, options).format(value);
|
|
36
|
+
}
|
|
37
|
+
if (format === 'date' || format === 'datetime') {
|
|
38
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
39
|
+
return '—';
|
|
40
|
+
const date = new Date(value);
|
|
41
|
+
if (!Number.isFinite(date.getTime()))
|
|
42
|
+
return '—';
|
|
43
|
+
return new Intl.DateTimeFormat(locale, format === 'date' ? { dateStyle: 'medium', timeZone: 'UTC' } : { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC' }).format(date);
|
|
44
|
+
}
|
|
45
|
+
return typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)) ? String(value) : '—';
|
|
46
|
+
}
|
|
47
|
+
export function buildSvarInteractiveColumns(columns, tree, editable, locale) {
|
|
48
|
+
return buildSvarColumns(columns, tree).map((column, index) => {
|
|
49
|
+
const source = columns[index];
|
|
50
|
+
if (!source)
|
|
51
|
+
throw new Error('Missing column');
|
|
52
|
+
return { ...column,
|
|
53
|
+
...(source.editable && editable ? { editor: 'text' } : {}),
|
|
54
|
+
...(source.format ? { template: (value) => formatSvarValue(value, source, locale) } : {}),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function record(value) {
|
|
59
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
export function resolveSvarCell(event, columns, index, primaryKey) {
|
|
62
|
+
const ev = record(event);
|
|
63
|
+
if (!ev || typeof ev['id'] !== 'string')
|
|
64
|
+
return;
|
|
65
|
+
const source = index.get(ev['id']);
|
|
66
|
+
const column = columns.find(item => svarColumnId(item.key) === ev['column'] && item.editable && item.key !== primaryKey && item.key !== 'id');
|
|
67
|
+
const id = source ? Object.getOwnPropertyDescriptor(source, primaryKey)?.value : undefined;
|
|
68
|
+
if (!column || (typeof id !== 'string' && typeof id !== 'number'))
|
|
69
|
+
return;
|
|
70
|
+
return { id, column };
|
|
71
|
+
}
|
|
72
|
+
export function parseSvarCellEdit(event, columns, index, primaryKey) {
|
|
73
|
+
const ev = record(event);
|
|
74
|
+
const cell = resolveSvarCell(event, columns, index, primaryKey);
|
|
75
|
+
if (!cell || !ev)
|
|
76
|
+
return;
|
|
77
|
+
const raw = ev['value'];
|
|
78
|
+
let value;
|
|
79
|
+
if (cell.column.editable === 'number') {
|
|
80
|
+
if (typeof raw !== 'string' && typeof raw !== 'number')
|
|
81
|
+
throw new Error('Invalid number');
|
|
82
|
+
if (typeof raw === 'string' && !raw.trim())
|
|
83
|
+
throw new Error('A number is required');
|
|
84
|
+
value = Number(raw);
|
|
85
|
+
if (!Number.isFinite(value))
|
|
86
|
+
throw new Error('Invalid number');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (typeof raw !== 'string')
|
|
90
|
+
throw new Error('Text is required');
|
|
91
|
+
value = raw;
|
|
92
|
+
}
|
|
93
|
+
return { id: cell.id, field: cell.column.key, value };
|
|
94
|
+
}
|
|
95
|
+
/** 引擎永远不能自行写业务数据;编辑只生成宿主请求,成功后由查询数据回填。 */
|
|
96
|
+
export function installSvarInteractions(api, options) {
|
|
97
|
+
const index = () => svarRecordIndex(options.items(), options.primaryKey(), options.childrenKey());
|
|
98
|
+
for (const action of SVAR_BLOCKED_ACTIONS) {
|
|
99
|
+
if (!['open-editor', 'editor', 'update-cell'].includes(action))
|
|
100
|
+
api.intercept(action, () => false);
|
|
101
|
+
}
|
|
102
|
+
api.intercept('open-editor', event => {
|
|
103
|
+
if (!options.current() || !options.editable())
|
|
104
|
+
return false;
|
|
105
|
+
try {
|
|
106
|
+
if (!resolveSvarCell(event, options.columns(), index(), options.primaryKey()))
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
api.intercept('editor', () => { if (!options.current() || !options.editable())
|
|
114
|
+
return false; });
|
|
115
|
+
api.intercept('update-cell', event => {
|
|
116
|
+
if (options.current() && options.editable()) {
|
|
117
|
+
try {
|
|
118
|
+
const edit = parseSvarCellEdit(event, options.columns(), index(), options.primaryKey());
|
|
119
|
+
if (edit)
|
|
120
|
+
options.edit(edit);
|
|
121
|
+
}
|
|
122
|
+
catch (failure) {
|
|
123
|
+
options.error(failure instanceof Error ? failure.message : 'Invalid edit');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
});
|
|
128
|
+
api.intercept('select-row', () => { if (!options.current() || !options.selectable())
|
|
129
|
+
return false; });
|
|
130
|
+
api.on('select-row', () => {
|
|
131
|
+
if (!options.current() || !options.selectable())
|
|
132
|
+
return;
|
|
133
|
+
const selected = api.getState().selectedRows;
|
|
134
|
+
if (!Array.isArray(selected))
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
const records = index();
|
|
138
|
+
const ids = selected.flatMap(key => {
|
|
139
|
+
const source = typeof key === 'string' ? records.get(key) : undefined;
|
|
140
|
+
const id = source ? Object.getOwnPropertyDescriptor(source, options.primaryKey())?.value : undefined;
|
|
141
|
+
return typeof id === 'string' || typeof id === 'number' ? [id] : [];
|
|
142
|
+
});
|
|
143
|
+
if (ids.length > 100) {
|
|
144
|
+
options.error('Select at most 100 records');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
options.selection(ids);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
options.selection([]);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
api.intercept('sort-rows', event => {
|
|
154
|
+
if (!options.current())
|
|
155
|
+
return false;
|
|
156
|
+
if (options.server()) {
|
|
157
|
+
const next = nextSvarSort(event, options.columns(), options.sorters());
|
|
158
|
+
if (next)
|
|
159
|
+
options.sort(next);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
api.intercept('filter-rows', event => {
|
|
164
|
+
if (!options.current())
|
|
165
|
+
return false;
|
|
166
|
+
if (options.server()) {
|
|
167
|
+
const next = nextSvarFilter(event, options.columns(), options.filters());
|
|
168
|
+
if (next)
|
|
169
|
+
options.filter(next);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export { svarRecordKey };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type SvarColumn, type SvarRow } from './svar-grid-contract.js';
|
|
2
|
+
import { type SvarRecordId } from './svar-grid-operations.js';
|
|
3
|
+
export interface SvarRange {
|
|
4
|
+
readonly start: number;
|
|
5
|
+
readonly end: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SvarWindowPage {
|
|
8
|
+
readonly data: readonly Record<string, unknown>[];
|
|
9
|
+
readonly total: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SvarWindowSource {
|
|
12
|
+
readonly total: number;
|
|
13
|
+
/** 可信宿主负责数据契约和后端授权,范围使用从零开始、右端不含的索引。 */
|
|
14
|
+
readonly load: (request: SvarRange & {
|
|
15
|
+
readonly signal: AbortSignal;
|
|
16
|
+
}) => Promise<SvarWindowPage>;
|
|
17
|
+
}
|
|
18
|
+
export type SvarChildrenLoader = (request: {
|
|
19
|
+
readonly id: SvarRecordId;
|
|
20
|
+
readonly signal: AbortSignal;
|
|
21
|
+
}) => Promise<readonly Record<string, unknown>[]>;
|
|
22
|
+
export declare class SvarLoadCancelled extends Error {
|
|
23
|
+
constructor();
|
|
24
|
+
}
|
|
25
|
+
/** 单个网格的有界缓存:同键请求去重,AbortSignal 加请求所有者双重隔离。 */
|
|
26
|
+
export declare function createSvarLoadCache<T>(copy: (value: T) => T, maxReady?: number, maxPending?: number): {
|
|
27
|
+
request: (key: string, load: (signal: AbortSignal) => Promise<T>) => Promise<T>;
|
|
28
|
+
clear: () => void;
|
|
29
|
+
invalidate(key: string): void;
|
|
30
|
+
abortPendingExcept(key: string): void;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
};
|
|
33
|
+
export declare function snapshotSvarRecords(items: readonly Record<string, unknown>[], primaryKey?: string, childrenKey?: string): Record<string, unknown>[];
|
|
34
|
+
export declare function checkedSvarRange(event: unknown, total: number): SvarRange;
|
|
35
|
+
export declare function snapshotSvarWindow(page: SvarWindowPage, range: SvarRange, primaryKey?: string): SvarWindowPage;
|
|
36
|
+
export declare function svarRowId(record: Record<string, unknown>, primaryKey: string): SvarRecordId;
|
|
37
|
+
/** 合并已加载分支但不修改 Provider 记录;最后全树验证,拒绝跨分支重复 ID。 */
|
|
38
|
+
export declare function mergeSvarBranches(items: readonly Record<string, unknown>[], branches: ReadonlyMap<string, readonly Record<string, unknown>[]>, primaryKey: string, childrenKey: string): Record<string, unknown>[];
|
|
39
|
+
/** 占位子节点永远不能被选中或编辑;真正展开前由适配器拦截并加载。 */
|
|
40
|
+
export declare function projectSvarLazyRows(records: readonly Record<string, unknown>[], columns: readonly SvarColumn[], primaryKey: string, childrenKey: string, hasChildrenKey: string, loaded: ReadonlyMap<string, readonly Record<string, unknown>[]>, opened: ReadonlySet<string>): SvarRow[];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { snapshotPlainData, decodeBaseRecord } from '@svadmin/core/schema';
|
|
2
|
+
import { projectSvarRows } from './svar-grid-contract.js';
|
|
3
|
+
import { svarRecordIndex, svarRecordKey } from './svar-grid-operations.js';
|
|
4
|
+
export class SvarLoadCancelled extends Error {
|
|
5
|
+
constructor() { super('Grid request was superseded'); this.name = 'SvarLoadCancelled'; }
|
|
6
|
+
}
|
|
7
|
+
/** 单个网格的有界缓存:同键请求去重,AbortSignal 加请求所有者双重隔离。 */
|
|
8
|
+
export function createSvarLoadCache(copy, maxReady = 32, maxPending = 8) {
|
|
9
|
+
const entries = new Map();
|
|
10
|
+
let disposed = false;
|
|
11
|
+
function clear() {
|
|
12
|
+
const pending = [...entries.values()];
|
|
13
|
+
entries.clear();
|
|
14
|
+
for (const entry of pending)
|
|
15
|
+
if (entry.kind === 'pending')
|
|
16
|
+
entry.controller.abort();
|
|
17
|
+
}
|
|
18
|
+
function trim() {
|
|
19
|
+
let count = [...entries.values()].filter(entry => entry.kind === 'ready').length;
|
|
20
|
+
for (const [key, entry] of entries) {
|
|
21
|
+
if (count <= maxReady)
|
|
22
|
+
break;
|
|
23
|
+
if (entry.kind === 'ready') {
|
|
24
|
+
entries.delete(key);
|
|
25
|
+
count--;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function request(key, load) {
|
|
30
|
+
if (disposed)
|
|
31
|
+
return Promise.reject(new SvarLoadCancelled());
|
|
32
|
+
const existing = entries.get(key);
|
|
33
|
+
if (existing?.kind === 'pending')
|
|
34
|
+
return existing.promise.then(copy);
|
|
35
|
+
if (existing?.kind === 'ready') {
|
|
36
|
+
entries.delete(key);
|
|
37
|
+
entries.set(key, existing);
|
|
38
|
+
return Promise.resolve(copy(existing.value));
|
|
39
|
+
}
|
|
40
|
+
if ([...entries.values()].filter(entry => entry.kind === 'pending').length >= maxPending) {
|
|
41
|
+
return Promise.reject(new Error('Too many simultaneous grid requests'));
|
|
42
|
+
}
|
|
43
|
+
const owner = {};
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const current = () => !disposed && !controller.signal.aborted && entries.get(key)?.owner === owner;
|
|
46
|
+
const promise = Promise.resolve().then(() => {
|
|
47
|
+
if (!current())
|
|
48
|
+
throw new SvarLoadCancelled();
|
|
49
|
+
return load(controller.signal);
|
|
50
|
+
}).then(value => {
|
|
51
|
+
if (!current())
|
|
52
|
+
throw new SvarLoadCancelled();
|
|
53
|
+
const cached = copy(value);
|
|
54
|
+
if (!current())
|
|
55
|
+
throw new SvarLoadCancelled();
|
|
56
|
+
entries.set(key, { kind: 'ready', owner, value: cached });
|
|
57
|
+
trim();
|
|
58
|
+
return copy(cached);
|
|
59
|
+
}).catch((failure) => {
|
|
60
|
+
const active = current();
|
|
61
|
+
if (entries.get(key)?.owner === owner)
|
|
62
|
+
entries.delete(key);
|
|
63
|
+
if (!active)
|
|
64
|
+
throw new SvarLoadCancelled();
|
|
65
|
+
throw failure;
|
|
66
|
+
});
|
|
67
|
+
entries.set(key, { kind: 'pending', owner, controller, promise });
|
|
68
|
+
return promise;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
request, clear,
|
|
72
|
+
invalidate(key) {
|
|
73
|
+
const entry = entries.get(key);
|
|
74
|
+
entries.delete(key);
|
|
75
|
+
if (entry?.kind === 'pending')
|
|
76
|
+
entry.controller.abort();
|
|
77
|
+
},
|
|
78
|
+
abortPendingExcept(key) {
|
|
79
|
+
for (const [id, entry] of entries) {
|
|
80
|
+
if (id !== key && entry.kind === 'pending') {
|
|
81
|
+
entries.delete(id);
|
|
82
|
+
entry.controller.abort();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
dispose() { disposed = true; clear(); },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function snapshotSvarRecords(items, primaryKey = 'id', childrenKey) {
|
|
90
|
+
const cloned = snapshotPlainData(items);
|
|
91
|
+
if (!Array.isArray(cloned) || cloned.length > 100_000)
|
|
92
|
+
throw new Error('Invalid grid records');
|
|
93
|
+
const records = cloned.map(value => decodeBaseRecord(value));
|
|
94
|
+
if (svarRecordIndex(records, primaryKey, childrenKey).size > 100_000)
|
|
95
|
+
throw new Error('Too many grid records');
|
|
96
|
+
return records;
|
|
97
|
+
}
|
|
98
|
+
export function checkedSvarRange(event, total) {
|
|
99
|
+
if (!Number.isSafeInteger(total) || total < 0 || total > 10_000_000)
|
|
100
|
+
throw new Error('Invalid total row count');
|
|
101
|
+
const row = event && typeof event === 'object' ? Object.getOwnPropertyDescriptor(event, 'row')?.value : undefined;
|
|
102
|
+
const start = row && typeof row === 'object' ? Object.getOwnPropertyDescriptor(row, 'start')?.value : undefined;
|
|
103
|
+
const end = row && typeof row === 'object' ? Object.getOwnPropertyDescriptor(row, 'end')?.value : undefined;
|
|
104
|
+
if (typeof start !== 'number' || typeof end !== 'number' || !Number.isSafeInteger(start) || !Number.isSafeInteger(end)
|
|
105
|
+
|| start < 0 || end < start || end - start > 2048)
|
|
106
|
+
throw new Error('Invalid grid window');
|
|
107
|
+
return { start: Math.min(start, total), end: Math.min(end, total) };
|
|
108
|
+
}
|
|
109
|
+
export function snapshotSvarWindow(page, range, primaryKey = 'id') {
|
|
110
|
+
const total = page && typeof page === 'object' ? Object.getOwnPropertyDescriptor(page, 'total')?.value : undefined;
|
|
111
|
+
const data = page && typeof page === 'object' ? Object.getOwnPropertyDescriptor(page, 'data')?.value : undefined;
|
|
112
|
+
if (typeof total !== 'number' || !Number.isSafeInteger(total) || total < 0 || total > 10_000_000 || !Array.isArray(data)
|
|
113
|
+
|| data.length !== Math.max(0, Math.min(range.end, total) - range.start))
|
|
114
|
+
throw new Error('Invalid window response');
|
|
115
|
+
return { data: snapshotSvarRecords(data, primaryKey), total };
|
|
116
|
+
}
|
|
117
|
+
export function svarRowId(record, primaryKey) {
|
|
118
|
+
const id = Object.getOwnPropertyDescriptor(record, primaryKey)?.value;
|
|
119
|
+
if (typeof id !== 'string' && typeof id !== 'number')
|
|
120
|
+
throw new Error('Invalid record ID');
|
|
121
|
+
svarRecordKey(id);
|
|
122
|
+
return id;
|
|
123
|
+
}
|
|
124
|
+
/** 合并已加载分支但不修改 Provider 记录;最后全树验证,拒绝跨分支重复 ID。 */
|
|
125
|
+
export function mergeSvarBranches(items, branches, primaryKey, childrenKey) {
|
|
126
|
+
if (['__proto__', 'prototype', 'constructor'].includes(childrenKey) || !childrenKey)
|
|
127
|
+
throw new Error('Invalid children field');
|
|
128
|
+
const copied = snapshotSvarRecords(items, primaryKey, childrenKey);
|
|
129
|
+
let count = 0;
|
|
130
|
+
const ancestors = new Set();
|
|
131
|
+
function visit(records, depth) {
|
|
132
|
+
if (depth > 64)
|
|
133
|
+
throw new Error('Grid tree is too deep');
|
|
134
|
+
return records.map(record => {
|
|
135
|
+
if (++count > 100_000)
|
|
136
|
+
throw new Error('Too many grid records');
|
|
137
|
+
const key = svarRecordKey(svarRowId(record, primaryKey));
|
|
138
|
+
if (ancestors.has(key))
|
|
139
|
+
throw new Error('Cyclic grid branch');
|
|
140
|
+
const extra = branches.get(key);
|
|
141
|
+
const native = record[childrenKey];
|
|
142
|
+
const children = extra === undefined ? (Array.isArray(native) ? native.map(decodeBaseRecord) : undefined)
|
|
143
|
+
: snapshotSvarRecords(extra, primaryKey, childrenKey);
|
|
144
|
+
if (children === undefined)
|
|
145
|
+
return record;
|
|
146
|
+
ancestors.add(key);
|
|
147
|
+
const next = { ...record, [childrenKey]: visit(children, depth + 1) };
|
|
148
|
+
ancestors.delete(key);
|
|
149
|
+
return next;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const combined = visit(copied, 0);
|
|
153
|
+
svarRecordIndex(combined, primaryKey, childrenKey);
|
|
154
|
+
return combined;
|
|
155
|
+
}
|
|
156
|
+
/** 占位子节点永远不能被选中或编辑;真正展开前由适配器拦截并加载。 */
|
|
157
|
+
export function projectSvarLazyRows(records, columns, primaryKey, childrenKey, hasChildrenKey, loaded, opened) {
|
|
158
|
+
if (!hasChildrenKey || ['__proto__', 'prototype', 'constructor'].includes(hasChildrenKey))
|
|
159
|
+
throw new Error('Invalid branch marker field');
|
|
160
|
+
const index = svarRecordIndex(records, primaryKey, childrenKey);
|
|
161
|
+
const rows = projectSvarRows(records, columns, primaryKey, childrenKey);
|
|
162
|
+
function visit(items) {
|
|
163
|
+
for (const row of items) {
|
|
164
|
+
if (row.data?.length) {
|
|
165
|
+
row.open = opened.has(row.id);
|
|
166
|
+
visit(row.data);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const record = index.get(row.id);
|
|
170
|
+
const marker = record ? Object.getOwnPropertyDescriptor(record, hasChildrenKey)?.value : undefined;
|
|
171
|
+
if (marker === true && !loaded.has(row.id)) {
|
|
172
|
+
row.data = [{ id: `pending:${row.id}` }];
|
|
173
|
+
row.open = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
visit(rows);
|
|
179
|
+
return rows;
|
|
180
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SvarColumn, SvarRow } from './svar-grid-contract.js';
|
|
2
|
+
export type SvarRecordId = string | number;
|
|
3
|
+
export interface SvarBatchResult {
|
|
4
|
+
readonly succeeded: SvarRecordId[];
|
|
5
|
+
readonly failed: {
|
|
6
|
+
id: SvarRecordId;
|
|
7
|
+
message: string;
|
|
8
|
+
uncertain: boolean;
|
|
9
|
+
}[];
|
|
10
|
+
readonly skipped: SvarRecordId[];
|
|
11
|
+
readonly cancelled: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function svarRecordKey(id: SvarRecordId): string;
|
|
14
|
+
/** 不解析引擎字符串推测业务 ID,必须从当前已授权记录建立映射。 */
|
|
15
|
+
export declare function svarRecordIndex(items: readonly Record<string, unknown>[], primaryKey?: string, childrenKey?: string): Map<string, Record<string, unknown>>;
|
|
16
|
+
export declare function checkedSvarSelection(ids: readonly SvarRecordId[], max?: number): SvarRecordId[];
|
|
17
|
+
/** 整批预检后逐条重新授权;部分成功不是事务回滚,会话变更后停止派发。 */
|
|
18
|
+
export declare function runSvarBatch(options: {
|
|
19
|
+
ids: readonly SvarRecordId[];
|
|
20
|
+
current: () => boolean;
|
|
21
|
+
authorize: (id: SvarRecordId) => Promise<boolean>;
|
|
22
|
+
write: (id: SvarRecordId) => Promise<unknown>;
|
|
23
|
+
}): Promise<SvarBatchResult>;
|
|
24
|
+
/** 导出只接受显式投影后的标量值;不调用对象的 toString/toJSON。 */
|
|
25
|
+
export declare function svarCsv(rows: readonly SvarRow[], columns: readonly SvarColumn[]): string;
|
|
26
|
+
export interface SvarExportSnapshot {
|
|
27
|
+
readonly ids: readonly SvarRecordId[];
|
|
28
|
+
readonly csv: string;
|
|
29
|
+
}
|
|
30
|
+
/** 在首次 await 之前冻结完整导出内容与所有后代 ID,避免检查与导出对象不一致。 */
|
|
31
|
+
export declare function prepareSvarExport(items: readonly Record<string, unknown>[], columns: readonly SvarColumn[], primaryKey?: string, childrenKey?: string): SvarExportSnapshot;
|
|
32
|
+
/** 只授权已经冻结的快照;禁止只检查树根,或在权限等待结束后读取新数据。 */
|
|
33
|
+
export declare function authorizeSvarExport(snapshot: SvarExportSnapshot, options: {
|
|
34
|
+
current: () => boolean;
|
|
35
|
+
authorize: (action: 'list' | 'export', id: SvarRecordId | undefined) => Promise<boolean>;
|
|
36
|
+
}): Promise<string>;
|
|
37
|
+
export declare function downloadSvarCsv(content: string, resourceName: string): void;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { checkedSvarColumns, projectSvarRows, svarColumnId } from './svar-grid-contract.js';
|
|
2
|
+
export function svarRecordKey(id) {
|
|
3
|
+
if ((typeof id !== 'string' || id.length === 0) && (typeof id !== 'number' || !Number.isFinite(id))) {
|
|
4
|
+
throw new Error('Invalid record ID');
|
|
5
|
+
}
|
|
6
|
+
return `${typeof id === 'number' ? 'n' : 's'}:${id}`;
|
|
7
|
+
}
|
|
8
|
+
/** 不解析引擎字符串推测业务 ID,必须从当前已授权记录建立映射。 */
|
|
9
|
+
export function svarRecordIndex(items, primaryKey = 'id', childrenKey) {
|
|
10
|
+
const index = new Map();
|
|
11
|
+
const ancestors = new Set();
|
|
12
|
+
function visit(records, depth) {
|
|
13
|
+
if (!Array.isArray(records) || depth > 64)
|
|
14
|
+
throw new Error('Invalid grid tree');
|
|
15
|
+
for (const record of records) {
|
|
16
|
+
if (!record || typeof record !== 'object' || Array.isArray(record) || ancestors.has(record))
|
|
17
|
+
throw new Error('Invalid grid tree');
|
|
18
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, primaryKey);
|
|
19
|
+
const id = descriptor && 'value' in descriptor ? descriptor.value : undefined;
|
|
20
|
+
if (typeof id !== 'string' && typeof id !== 'number')
|
|
21
|
+
throw new Error('Invalid record ID');
|
|
22
|
+
const key = svarRecordKey(id);
|
|
23
|
+
if (index.has(key))
|
|
24
|
+
throw new Error('Duplicate record ID');
|
|
25
|
+
index.set(key, record);
|
|
26
|
+
if (childrenKey) {
|
|
27
|
+
const childDescriptor = Object.getOwnPropertyDescriptor(record, childrenKey);
|
|
28
|
+
if (childDescriptor && !('value' in childDescriptor))
|
|
29
|
+
throw new Error('Accessors are not allowed');
|
|
30
|
+
const children = childDescriptor?.value;
|
|
31
|
+
if (children !== undefined && !Array.isArray(children))
|
|
32
|
+
throw new Error('Invalid child records');
|
|
33
|
+
if (Array.isArray(children)) {
|
|
34
|
+
ancestors.add(record);
|
|
35
|
+
visit(children, depth + 1);
|
|
36
|
+
ancestors.delete(record);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
visit(items, 0);
|
|
42
|
+
return index;
|
|
43
|
+
}
|
|
44
|
+
export function checkedSvarSelection(ids, max = 100) {
|
|
45
|
+
if (!Array.isArray(ids) || ids.length > max)
|
|
46
|
+
throw new Error(`Select at most ${max} records`);
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
return ids.filter(id => {
|
|
49
|
+
const key = svarRecordKey(id);
|
|
50
|
+
if (seen.has(key))
|
|
51
|
+
return false;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** 整批预检后逐条重新授权;部分成功不是事务回滚,会话变更后停止派发。 */
|
|
57
|
+
export async function runSvarBatch(options) {
|
|
58
|
+
const ids = checkedSvarSelection(options.ids);
|
|
59
|
+
const succeeded = [];
|
|
60
|
+
const failed = [];
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
for (const id of ids) {
|
|
63
|
+
if (!options.current())
|
|
64
|
+
return { succeeded, failed, skipped: ids, cancelled: true };
|
|
65
|
+
const allowed = await options.authorize(id).catch(() => false);
|
|
66
|
+
if (!options.current())
|
|
67
|
+
return { succeeded, failed, skipped: ids, cancelled: true };
|
|
68
|
+
if (!allowed)
|
|
69
|
+
return { succeeded, failed: [{ id, message: 'Access denied', uncertain: false }], skipped: ids.filter(value => !Object.is(value, id)), cancelled: false };
|
|
70
|
+
}
|
|
71
|
+
for (const id of ids) {
|
|
72
|
+
if (!options.current()) {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const allowed = await options.authorize(id).catch(() => false);
|
|
77
|
+
if (!options.current()) {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!allowed) {
|
|
82
|
+
failed.push({ id, message: 'Access denied', uncertain: false });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await options.write(id);
|
|
87
|
+
succeeded.push(id);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const details = error && typeof error === 'object' ? Object.getOwnPropertyDescriptor(error, 'details')?.value : undefined;
|
|
91
|
+
const known = details && typeof details === 'object' ? Object.getOwnPropertyDescriptor(details, 'writeMayHaveSucceeded')?.value : undefined;
|
|
92
|
+
failed.push({ id, message: 'Operation failed; refresh before retrying', uncertain: known !== false });
|
|
93
|
+
}
|
|
94
|
+
if (!options.current()) {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const attempted = new Set([...succeeded, ...failed.map(item => item.id)].map(svarRecordKey));
|
|
100
|
+
return { succeeded, failed, skipped: ids.filter(id => !attempted.has(svarRecordKey(id))), cancelled };
|
|
101
|
+
}
|
|
102
|
+
function hasFormulaPrefix(text) {
|
|
103
|
+
if (text.startsWith('\t') || text.startsWith('\r') || text.startsWith('\n'))
|
|
104
|
+
return true;
|
|
105
|
+
let offset = 0;
|
|
106
|
+
while (offset < text.length) {
|
|
107
|
+
const character = text[offset] ?? '';
|
|
108
|
+
if (text.charCodeAt(offset) <= 31 || /\s/u.test(character))
|
|
109
|
+
offset++;
|
|
110
|
+
else
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const first = text[offset];
|
|
114
|
+
return first !== undefined && '=+@-'.includes(first);
|
|
115
|
+
}
|
|
116
|
+
/** 导出只接受显式投影后的标量值;不调用对象的 toString/toJSON。 */
|
|
117
|
+
export function svarCsv(rows, columns) {
|
|
118
|
+
const cell = (value) => {
|
|
119
|
+
let text = typeof value === 'string' ? value
|
|
120
|
+
: typeof value === 'number' && Number.isFinite(value) ? String(value)
|
|
121
|
+
: typeof value === 'boolean' ? String(value) : '';
|
|
122
|
+
if (typeof value === 'string' && hasFormulaPrefix(text))
|
|
123
|
+
text = `'${text}`;
|
|
124
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
125
|
+
};
|
|
126
|
+
const lines = [columns.map(column => cell(column.label)).join(',')];
|
|
127
|
+
let count = 0;
|
|
128
|
+
const ancestors = new Set();
|
|
129
|
+
function visit(records, depth) {
|
|
130
|
+
if (depth > 64)
|
|
131
|
+
throw new Error('Export tree is too deep');
|
|
132
|
+
for (const row of records) {
|
|
133
|
+
if (++count > 100_000 || ancestors.has(row))
|
|
134
|
+
throw new Error('Invalid or oversized export');
|
|
135
|
+
lines.push(columns.map(column => cell(row[svarColumnId(column.key)])).join(','));
|
|
136
|
+
if (row.data) {
|
|
137
|
+
ancestors.add(row);
|
|
138
|
+
visit(row.data, depth + 1);
|
|
139
|
+
ancestors.delete(row);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
visit(rows, 0);
|
|
144
|
+
return `\uFEFF${lines.join('\r\n')}\r\n`;
|
|
145
|
+
}
|
|
146
|
+
/** 在首次 await 之前冻结完整导出内容与所有后代 ID,避免检查与导出对象不一致。 */
|
|
147
|
+
export function prepareSvarExport(items, columns, primaryKey = 'id', childrenKey) {
|
|
148
|
+
const checked = checkedSvarColumns(columns);
|
|
149
|
+
const index = svarRecordIndex(items, primaryKey, childrenKey);
|
|
150
|
+
const ids = [];
|
|
151
|
+
for (const record of index.values()) {
|
|
152
|
+
const id = Object.getOwnPropertyDescriptor(record, primaryKey)?.value;
|
|
153
|
+
if (typeof id !== 'string' && typeof id !== 'number')
|
|
154
|
+
throw new Error('Invalid record ID');
|
|
155
|
+
ids.push(id);
|
|
156
|
+
}
|
|
157
|
+
const csv = svarCsv(projectSvarRows(items, checked, primaryKey, childrenKey), checked);
|
|
158
|
+
return Object.freeze({ ids: Object.freeze(ids), csv });
|
|
159
|
+
}
|
|
160
|
+
/** 只授权已经冻结的快照;禁止只检查树根,或在权限等待结束后读取新数据。 */
|
|
161
|
+
export async function authorizeSvarExport(snapshot, options) {
|
|
162
|
+
async function check(action, id) {
|
|
163
|
+
if (!options.current())
|
|
164
|
+
throw new Error('Export scope changed');
|
|
165
|
+
const allowed = await options.authorize(action, id);
|
|
166
|
+
if (!options.current() || !allowed)
|
|
167
|
+
throw new Error('Export access denied');
|
|
168
|
+
}
|
|
169
|
+
await check('list');
|
|
170
|
+
await check('export');
|
|
171
|
+
for (const id of snapshot.ids)
|
|
172
|
+
await check('export', id);
|
|
173
|
+
if (!options.current())
|
|
174
|
+
throw new Error('Export scope changed');
|
|
175
|
+
return snapshot.csv;
|
|
176
|
+
}
|
|
177
|
+
export function downloadSvarCsv(content, resourceName) {
|
|
178
|
+
const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }));
|
|
179
|
+
const anchor = document.createElement('a');
|
|
180
|
+
anchor.href = url;
|
|
181
|
+
anchor.download = `${resourceName.replace(/[^a-zA-Z0-9_-]/gu, '_').slice(0, 80) || 'records'}.csv`;
|
|
182
|
+
document.body.append(anchor);
|
|
183
|
+
anchor.click();
|
|
184
|
+
anchor.remove();
|
|
185
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
186
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { SvarInteractiveApi, SvarInteractiveEngineProps } from './svar-grid-interactions.js';
|
|
3
|
+
export interface SvarManagedApi extends SvarInteractiveApi {
|
|
4
|
+
exec(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
5
|
+
getState(): {
|
|
6
|
+
selectedRows?: unknown;
|
|
7
|
+
scrollTop?: number;
|
|
8
|
+
flatData?: Record<string, unknown>[];
|
|
9
|
+
focusCell?: {
|
|
10
|
+
row?: string | number;
|
|
11
|
+
column?: string | number;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface SvarManagedEngineProps extends Omit<SvarInteractiveEngineProps, 'init' | 'columns'> {
|
|
16
|
+
columns: (SvarInteractiveEngineProps['columns'][number] & {
|
|
17
|
+
hidden?: boolean;
|
|
18
|
+
cell?: Component<{
|
|
19
|
+
row: unknown;
|
|
20
|
+
column: unknown;
|
|
21
|
+
}>;
|
|
22
|
+
})[];
|
|
23
|
+
dynamic?: {
|
|
24
|
+
rowCount?: number;
|
|
25
|
+
};
|
|
26
|
+
init?: (api: SvarManagedApi) => void;
|
|
27
|
+
}
|
|
28
|
+
/** 两个公开 Grid 实例组成固定右侧面板,不读取或改写上游 Community 功能开关。 */
|
|
29
|
+
export declare function createSvarPanePair(current: () => boolean): {
|
|
30
|
+
attach: (side: 0 | 1, api: SvarManagedApi) => void;
|
|
31
|
+
focus: (side: 0 | 1, row: string | number, column: string) => void;
|
|
32
|
+
get: (side: 0 | 1) => SvarManagedApi | undefined;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
};
|