@svgrid/enterprise 3.1.0 → 3.1.2
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/SvSheet.svelte +93 -8
- package/dist/cdn/svgrid-enterprise.svelte-external.js +9339 -9077
- package/dist/designer/assets/{GridMenus-8gTNssgV.js → GridMenus-DGZJKFvX.js} +1 -1
- package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +1 -0
- package/dist/designer/assets/{SvGridCellEditor-DMoWJjEI.js → SvGridCellEditor-C7NWcnNC.js} +1 -1
- package/dist/designer/assets/{SvGridChart-Cy1ATrWh.js → SvGridChart-Fg43Yi0M.js} +2 -2
- package/dist/designer/assets/{SvGridChartBuilder-eQ3mn0L9.js → SvGridChartBuilder-BqGuA3yU.js} +1 -1
- package/dist/designer/assets/{SvGridChartPanel-CATdVc9X.js → SvGridChartPanel-BsiXRAnx.js} +3 -3
- package/dist/designer/assets/{SvGridChartView-CmsZtCED.js → SvGridChartView-BgttK0tR.js} +1 -1
- package/dist/designer/assets/{SvListBox-BDS66lmZ.js → SvListBox-v2jm0-vf.js} +1 -1
- package/dist/designer/assets/cell-formatting-BWrW_O0n.js +1 -0
- package/dist/designer/assets/{chart-Cn7MnNqA.js → chart-BIl-dIRv.js} +1 -1
- package/dist/designer/assets/{chart-panel-messages-Dz7PTIqT.js → chart-panel-messages-4cF2vw7L.js} +1 -1
- package/dist/designer/assets/{chart-summary-BD1qYB2R.js → chart-summary-B7bTtSCC.js} +1 -1
- package/dist/designer/assets/{chart-trend-B8xr5LuD.js → chart-trend-CLsn5Hgd.js} +1 -1
- package/dist/designer/assets/export-format-DGscOjS9.js +6 -0
- package/dist/designer/assets/{index-BSjv6Qec.js → index-V_cMXdxt.js} +314 -314
- package/dist/designer/assets/index-l21iGkpF.css +1 -0
- package/dist/designer/assets/src-Mu7AGqT8.js +58 -0
- package/dist/designer/assets/validate-YxSrBF1H.js +1 -0
- package/dist/designer/index.html +12 -12
- package/dist/export-ooxml.d.ts +10 -0
- package/dist/export-ooxml.js +22 -5
- package/dist/import.js +11 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/node/studio.js +236 -132
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/server-row-model.d.ts +9 -0
- package/dist/server/server-row-model.js +120 -3
- package/dist/server/worker-data-source.d.ts +62 -0
- package/dist/server/worker-data-source.js +133 -0
- package/dist/sheet/objects.d.ts +47 -0
- package/dist/sheet/objects.js +33 -0
- package/dist/studio/cli.js +55 -1
- package/dist/studio/emit-project.js +8 -3
- package/dist/studio/init-flow.d.ts +6 -1
- package/dist/studio/init-flow.js +43 -2
- package/dist/studio/scaffold-app.d.ts +6 -0
- package/dist/studio/scaffold-app.js +5 -3
- package/dist/studio/scaffold.js +29 -12
- package/dist/sveltekit/in-memory.d.ts +21 -1
- package/dist/sveltekit/in-memory.js +153 -105
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/version.d.ts +14 -1
- package/dist/version.js +14 -1
- package/dist/wc/{GridMenus-By9vPuq0.js → GridMenus-6_fF5jIm.js} +2 -2
- package/dist/wc/{SvGrid.helpers-BdVDtjUm.js → SvGrid.helpers-zBkDTryR.js} +36 -37
- package/dist/wc/{SvGridCellEditor-Bnf1Kr5C.js → SvGridCellEditor-CzUKEbkI.js} +1 -1
- package/dist/wc/{SvGridChart-BqUJh2pG.js → SvGridChart-C6UvNIbN.js} +3 -3
- package/dist/wc/{SvGridChartBuilder-CGycC5cn.js → SvGridChartBuilder-ikf_5LyW.js} +3 -3
- package/dist/wc/{SvGridChartPanel-CRnizsHD.js → SvGridChartPanel-Dc_jj7B-.js} +4 -4
- package/dist/wc/{SvGridChartView-D1pa_xMy.js → SvGridChartView-thkznAOb.js} +2 -2
- package/dist/wc/{cell-formatting-CBfgahv1.js → cell-formatting-hb3vzxhJ.js} +27 -5
- package/dist/wc/{chart-6OXlybVT.js → chart-CV5D7_k8.js} +1 -1
- package/dist/wc/{chart-summary-2m3ia1-n.js → chart-summary-DGx0QhJa.js} +1 -1
- package/dist/wc/{chart-trend-CQOQtTMl.js → chart-trend-BIRH_xH8.js} +1 -1
- package/dist/wc/sv-sheet-element.js +7454 -7352
- package/dist/wc/{svelte-virtualizer.svelte-CXtQy2TQ.js → svelte-virtualizer.svelte-Du4tjrIP.js} +7 -3
- package/dist/wc/{validate-BqDthkNr.js → validate-JAuMsk6Z.js} +36 -36
- package/dist/worker/columnar.d.ts +42 -0
- package/dist/worker/columnar.js +410 -0
- package/dist/worker/index.d.ts +12 -0
- package/dist/worker/index.js +12 -0
- package/dist/worker/infer-schema.d.ts +21 -0
- package/dist/worker/infer-schema.js +34 -0
- package/dist/worker/protocol.d.ts +73 -0
- package/dist/worker/protocol.js +14 -0
- package/dist/worker/serve.d.ts +44 -0
- package/dist/worker/serve.js +100 -0
- package/package.json +7 -3
- package/dist/designer/assets/SvGrid.helpers-BgT-gnCo.js +0 -1
- package/dist/designer/assets/cell-formatting-BbubJTVz.js +0 -1
- package/dist/designer/assets/export-format-DGckmgKr.js +0 -6
- package/dist/designer/assets/index-_JVFxt02.css +0 -1
- package/dist/designer/assets/src-DUQ4xE3x.js +0 -58
- package/dist/designer/assets/validate-C3B8QKsM.js +0 -1
|
@@ -368,10 +368,53 @@ export function createServerRowModel(source, options) {
|
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
|
-
|
|
371
|
+
const leafRuns = new WeakMap();
|
|
372
|
+
function leafRun(store, rows) {
|
|
373
|
+
if (openDetails.size)
|
|
374
|
+
return null;
|
|
375
|
+
const prev = leafRuns.get(store);
|
|
376
|
+
if (prev && prev.rows === rows)
|
|
377
|
+
return prev;
|
|
378
|
+
const n = rows.length;
|
|
379
|
+
// Same length (a block landed, a row was patched): update the kept
|
|
380
|
+
// arrays in place. The flatten only ever copies out of them.
|
|
381
|
+
const inPlace = !!prev && prev.rows.length === n;
|
|
382
|
+
const display = inPlace ? prev.display : new Array(n);
|
|
383
|
+
const grid = inPlace ? prev.grid : new Array(n);
|
|
384
|
+
const old = prev?.rows;
|
|
385
|
+
const oldLength = old ? old.length : 0;
|
|
386
|
+
for (let i = 0; i < n; i += 1) {
|
|
387
|
+
const child = rows[i];
|
|
388
|
+
if (i < oldLength && old[i] === child) {
|
|
389
|
+
if (!inPlace) {
|
|
390
|
+
display[i] = prev.display[i];
|
|
391
|
+
grid[i] = prev.grid[i];
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const ph = rowPlaceholderState(child);
|
|
396
|
+
if (ph) {
|
|
397
|
+
const d = ph === 'failed' ? FAILED_ROW : LOADING_ROW;
|
|
398
|
+
display[i] = d;
|
|
399
|
+
grid[i] = toGridRow(d);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (child.kind !== 'leaf') {
|
|
403
|
+
// Written in place up to here, so the kept run is no longer whole.
|
|
404
|
+
leafRuns.delete(store);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const d = { kind: 'leaf', id: child.id, level: store.level, route: store.route, data: child.data };
|
|
408
|
+
display[i] = d;
|
|
409
|
+
grid[i] = toGridRow(d, child.data);
|
|
410
|
+
}
|
|
411
|
+
const run = { rows, display, grid };
|
|
412
|
+
leafRuns.set(store, run);
|
|
413
|
+
return run;
|
|
414
|
+
}
|
|
372
415
|
function flatten() {
|
|
373
|
-
|
|
374
|
-
|
|
416
|
+
let display = [];
|
|
417
|
+
let grid = [];
|
|
375
418
|
segments = [];
|
|
376
419
|
const pushRow = (d, data) => {
|
|
377
420
|
display.push(d);
|
|
@@ -392,6 +435,28 @@ export function createServerRowModel(source, options) {
|
|
|
392
435
|
};
|
|
393
436
|
const walk = (store, from = 0, to = Number.POSITIVE_INFINITY) => {
|
|
394
437
|
const rows = store.cache.rows();
|
|
438
|
+
const run = store.loadMore ? null : leafRun(store, rows);
|
|
439
|
+
if (run) {
|
|
440
|
+
// A level of leaves maps one to one: copy the range out of the run.
|
|
441
|
+
const end = Math.min(rows.length, to);
|
|
442
|
+
if (end > from) {
|
|
443
|
+
segments.push({ store, displayStart: display.length, storeStart: from, length: end - from });
|
|
444
|
+
const { display: d, grid: g } = run;
|
|
445
|
+
if (display.length === 0) {
|
|
446
|
+
// The common flat case: the whole list is this run. A slice is a
|
|
447
|
+
// block copy; a push per row was the rest of the flatten's cost.
|
|
448
|
+
display = d.slice(from, end);
|
|
449
|
+
grid = g.slice(from, end);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
for (let i = from; i < end; i += 1) {
|
|
453
|
+
display.push(d[i]);
|
|
454
|
+
grid.push(g[i]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
395
460
|
const end = Math.min(rows.length, to, store.loadMore ? loadedPrefix(rows) : Number.POSITIVE_INFINITY);
|
|
396
461
|
let segStart = from;
|
|
397
462
|
let dispStart = display.length;
|
|
@@ -606,6 +671,34 @@ export function createServerRowModel(source, options) {
|
|
|
606
671
|
pagination: pagination(),
|
|
607
672
|
};
|
|
608
673
|
}
|
|
674
|
+
// `keepRowsWhileLoading`: after a sort, a filter or a regroup, the rows on
|
|
675
|
+
// screen stay until the new answer's first block lands or the time runs
|
|
676
|
+
// out. `holdUntil` is that deadline, 0 when nothing is held.
|
|
677
|
+
let holdUntil = 0;
|
|
678
|
+
let holdTimer = null;
|
|
679
|
+
let held = false;
|
|
680
|
+
function holdRows() {
|
|
681
|
+
const ms = options.keepRowsWhileLoading ?? 0;
|
|
682
|
+
if (ms > 0 && current.displayRows.length > 0)
|
|
683
|
+
holdUntil = Date.now() + ms;
|
|
684
|
+
}
|
|
685
|
+
/** Whether the rows on screen should stay a little longer. */
|
|
686
|
+
function holding() {
|
|
687
|
+
if (!holdUntil)
|
|
688
|
+
return false;
|
|
689
|
+
const root = stores.get(routeKey([]));
|
|
690
|
+
// A failed block ends the hold too: its Retry is the answer to show.
|
|
691
|
+
const rootAnswered = !!root && root.cache.getCacheState().some((b) => b.status !== 'loading');
|
|
692
|
+
let pending = false;
|
|
693
|
+
for (const s of stores.values())
|
|
694
|
+
if (s.refreshPending)
|
|
695
|
+
pending = true;
|
|
696
|
+
if (Date.now() >= holdUntil || rootError || (rootAnswered && !pending)) {
|
|
697
|
+
holdUntil = 0;
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
609
702
|
function scheduleEmit() {
|
|
610
703
|
if (disposed || emitScheduled)
|
|
611
704
|
return;
|
|
@@ -614,7 +707,25 @@ export function createServerRowModel(source, options) {
|
|
|
614
707
|
emitScheduled = false;
|
|
615
708
|
if (disposed)
|
|
616
709
|
return;
|
|
710
|
+
if (holding()) {
|
|
711
|
+
held = true;
|
|
712
|
+
holdTimer ?? (holdTimer = setTimeout(() => {
|
|
713
|
+
holdTimer = null;
|
|
714
|
+
scheduleEmit();
|
|
715
|
+
}, Math.max(0, holdUntil - Date.now())));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (holdTimer) {
|
|
719
|
+
clearTimeout(holdTimer);
|
|
720
|
+
holdTimer = null;
|
|
721
|
+
}
|
|
617
722
|
current = buildState();
|
|
723
|
+
// While rows were held the grid kept reporting positions in the old
|
|
724
|
+
// list, mapped through the old segments; map them through the new.
|
|
725
|
+
if (held) {
|
|
726
|
+
held = false;
|
|
727
|
+
applyViewport();
|
|
728
|
+
}
|
|
618
729
|
options.onChange?.(current);
|
|
619
730
|
for (const notify of subscribers)
|
|
620
731
|
notify();
|
|
@@ -749,6 +860,7 @@ export function createServerRowModel(source, options) {
|
|
|
749
860
|
}
|
|
750
861
|
function setSort(next) {
|
|
751
862
|
sortModel = next;
|
|
863
|
+
holdRows();
|
|
752
864
|
for (const store of storesAffectedBySort(next)) {
|
|
753
865
|
if (options.clientSideSort && sortLocally(store))
|
|
754
866
|
continue;
|
|
@@ -759,6 +871,7 @@ export function createServerRowModel(source, options) {
|
|
|
759
871
|
}
|
|
760
872
|
function setFilter(next) {
|
|
761
873
|
const previous = filterModel;
|
|
874
|
+
holdRows();
|
|
762
875
|
filterModel = Array.isArray(next.columns)
|
|
763
876
|
? { global: next.global, columns: toServerFilterColumns(next) }
|
|
764
877
|
: next;
|
|
@@ -1212,6 +1325,7 @@ export function createServerRowModel(source, options) {
|
|
|
1212
1325
|
expanded.clear();
|
|
1213
1326
|
seenGroups.clear();
|
|
1214
1327
|
}
|
|
1328
|
+
holdRows();
|
|
1215
1329
|
rebuild();
|
|
1216
1330
|
},
|
|
1217
1331
|
toggleGroup(row) {
|
|
@@ -1439,6 +1553,9 @@ export function createServerRowModel(source, options) {
|
|
|
1439
1553
|
if (asyncTimer)
|
|
1440
1554
|
clearTimeout(asyncTimer);
|
|
1441
1555
|
asyncTimer = null;
|
|
1556
|
+
if (holdTimer)
|
|
1557
|
+
clearTimeout(holdTimer);
|
|
1558
|
+
holdTimer = null;
|
|
1442
1559
|
asyncQueue.length = 0;
|
|
1443
1560
|
for (const s of stores.values())
|
|
1444
1561
|
s.cache.dispose();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `ServerDataSource` whose rows live in a Web Worker.
|
|
3
|
+
*
|
|
4
|
+
* Sorting, filtering, grouping and aggregating a few hundred thousand rows
|
|
5
|
+
* takes long enough on the main thread that scrolling and typing stop while
|
|
6
|
+
* it runs. This moves that work into a worker and lets the grid treat the
|
|
7
|
+
* worker as a server: `createServerRowModel` (or the free
|
|
8
|
+
* `createServerDataSource`) asks for one block of rows at a time, the worker
|
|
9
|
+
* answers with just that block, and the full array never crosses back.
|
|
10
|
+
*
|
|
11
|
+
* Inside the worker the requests run through `createInMemoryDataSource`, the
|
|
12
|
+
* reference implementation of the datasource contract, so the answers are the
|
|
13
|
+
* ones every other backend is tested against.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* // grid.worker.ts
|
|
17
|
+
* import { serveWorkerDataSource } from '@svgrid/enterprise/worker'
|
|
18
|
+
* serveWorkerDataSource({ rows: () => fetch('/orders.json').then((r) => r.json()) })
|
|
19
|
+
*
|
|
20
|
+
* // App.svelte
|
|
21
|
+
* const worker = new Worker(new URL('./grid.worker.ts', import.meta.url), { type: 'module' })
|
|
22
|
+
* const ctl = createServerRowModel(createWorkerDataSource(worker), { groupBy: ['region'] })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* What it cannot do: run your functions. A worker receives data, not code,
|
|
26
|
+
* so a column's `valueGetter`, `comparator` or custom filter function never
|
|
27
|
+
* reaches it. Sorting and filtering go by field value, as with any server.
|
|
28
|
+
*/
|
|
29
|
+
import type { RowData } from '@svgrid/grid';
|
|
30
|
+
import type { ServerDataSource } from '@svgrid/grid/server';
|
|
31
|
+
import type { EntitySchema } from '../schema.js';
|
|
32
|
+
import type { AggregateBucket, AggregateRequest } from '../sources/aggregate.js';
|
|
33
|
+
/** What `createWorkerDataSource` needs from a `Worker`. A `MessagePort` fits too. */
|
|
34
|
+
export type WorkerLike = {
|
|
35
|
+
postMessage(message: unknown): void;
|
|
36
|
+
addEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
|
37
|
+
removeEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
|
38
|
+
terminate?(): void;
|
|
39
|
+
};
|
|
40
|
+
export type WorkerDataSourceOptions<TData extends RowData> = {
|
|
41
|
+
/**
|
|
42
|
+
* Rows to copy into the worker. Copying is structured cloning on the main
|
|
43
|
+
* thread, so for a large dataset prefer loading it inside the worker
|
|
44
|
+
* (`serveWorkerDataSource({ rows })`) and leave this out.
|
|
45
|
+
*/
|
|
46
|
+
rows?: ReadonlyArray<TData>;
|
|
47
|
+
/** Sent with `rows`. Default: the worker's own, or one read off the rows. */
|
|
48
|
+
schema?: EntitySchema<TData>;
|
|
49
|
+
/** The row id field, when the schema is inferred. */
|
|
50
|
+
idField?: string;
|
|
51
|
+
};
|
|
52
|
+
export type WorkerDataSource<TData extends RowData> = Required<Pick<ServerDataSource<TData>, 'getRows' | 'createRow' | 'updateRow' | 'deleteRow' | 'updateWhere'>> & {
|
|
53
|
+
/** Group and reduce in the worker, for `SvSchemaChart` and dashboards. */
|
|
54
|
+
getAggregate(request: AggregateRequest): Promise<AggregateBucket[]>;
|
|
55
|
+
/** Settles once the worker holds rows: with the row count, or the loader's error. */
|
|
56
|
+
readonly ready: Promise<number>;
|
|
57
|
+
/** Replace the worker's rows. Resolves with the new count. */
|
|
58
|
+
setRows(rows: ReadonlyArray<TData>, schema?: EntitySchema<TData>): Promise<number>;
|
|
59
|
+
/** Reject what is pending and terminate the worker. The row model's `dispose()` calls it. */
|
|
60
|
+
destroy(): void;
|
|
61
|
+
};
|
|
62
|
+
export declare function createWorkerDataSource<TData extends RowData>(worker: WorkerLike, options?: WorkerDataSourceOptions<TData>): WorkerDataSource<TData>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CHANNEL, fromWorkerError, isChannelMessage, } from '../worker/protocol.js';
|
|
2
|
+
import { nudgeServerRowModel } from './enable.js';
|
|
3
|
+
function abortReason(signal) {
|
|
4
|
+
return signal.reason ?? new DOMException('The request was aborted.', 'AbortError');
|
|
5
|
+
}
|
|
6
|
+
export function createWorkerDataSource(worker, options = {}) {
|
|
7
|
+
nudgeServerRowModel();
|
|
8
|
+
let nextId = 1;
|
|
9
|
+
let destroyed = false;
|
|
10
|
+
// Set once the worker cannot answer any more: it failed to start, its
|
|
11
|
+
// loader threw, or `destroy()` ran. Every later call rejects with it.
|
|
12
|
+
let failure = null;
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
let resolveReady;
|
|
15
|
+
let rejectReady;
|
|
16
|
+
const ready = new Promise((resolve, reject) => {
|
|
17
|
+
resolveReady = resolve;
|
|
18
|
+
rejectReady = reject;
|
|
19
|
+
});
|
|
20
|
+
// A source nobody awaited `ready` on must not raise an unhandled rejection.
|
|
21
|
+
ready.catch(() => { });
|
|
22
|
+
function failAll(error) {
|
|
23
|
+
failure ?? (failure = error);
|
|
24
|
+
rejectReady(error);
|
|
25
|
+
for (const p of pending.values())
|
|
26
|
+
p.reject(error);
|
|
27
|
+
pending.clear();
|
|
28
|
+
}
|
|
29
|
+
function onMessage(event) {
|
|
30
|
+
const message = event.data;
|
|
31
|
+
if (!isChannelMessage(message))
|
|
32
|
+
return;
|
|
33
|
+
switch (message.kind) {
|
|
34
|
+
case 'ready':
|
|
35
|
+
resolveReady(message.rowCount);
|
|
36
|
+
break;
|
|
37
|
+
case 'failed':
|
|
38
|
+
failAll(fromWorkerError(message.error));
|
|
39
|
+
break;
|
|
40
|
+
case 'result': {
|
|
41
|
+
const p = pending.get(message.id);
|
|
42
|
+
if (!p)
|
|
43
|
+
return; // cancelled, or answered after destroy
|
|
44
|
+
pending.delete(message.id);
|
|
45
|
+
if (message.ok)
|
|
46
|
+
p.resolve(message.value);
|
|
47
|
+
else
|
|
48
|
+
p.reject(fromWorkerError(message.error));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// The worker script failed to load or threw at the top level. Without this
|
|
54
|
+
// every request would wait forever on a worker that is not there.
|
|
55
|
+
function onError(event) {
|
|
56
|
+
const message = event.message || 'The data source worker failed to start.';
|
|
57
|
+
failAll(new Error(message));
|
|
58
|
+
}
|
|
59
|
+
worker.addEventListener('message', onMessage);
|
|
60
|
+
worker.addEventListener('error', onError);
|
|
61
|
+
worker.start?.();
|
|
62
|
+
function send(message) {
|
|
63
|
+
worker.postMessage(message);
|
|
64
|
+
}
|
|
65
|
+
function request(message, signal) {
|
|
66
|
+
if (failure)
|
|
67
|
+
return Promise.reject(failure);
|
|
68
|
+
if (signal?.aborted)
|
|
69
|
+
return Promise.reject(abortReason(signal));
|
|
70
|
+
const id = nextId++;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const onAbort = () => {
|
|
73
|
+
if (!pending.delete(id))
|
|
74
|
+
return;
|
|
75
|
+
send({ channel: CHANNEL, kind: 'cancel', id });
|
|
76
|
+
reject(abortReason(signal));
|
|
77
|
+
};
|
|
78
|
+
pending.set(id, {
|
|
79
|
+
resolve: (value) => {
|
|
80
|
+
signal?.removeEventListener('abort', onAbort);
|
|
81
|
+
resolve(value);
|
|
82
|
+
},
|
|
83
|
+
reject: (error) => {
|
|
84
|
+
signal?.removeEventListener('abort', onAbort);
|
|
85
|
+
reject(error);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
89
|
+
send(message(id));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function call(method, args, signal) {
|
|
93
|
+
return request((id) => ({ channel: CHANNEL, kind: 'call', id, method, args }), signal);
|
|
94
|
+
}
|
|
95
|
+
function load(rows, schema) {
|
|
96
|
+
return request((id) => ({
|
|
97
|
+
channel: CHANNEL,
|
|
98
|
+
kind: 'load',
|
|
99
|
+
id,
|
|
100
|
+
rows,
|
|
101
|
+
...(schema ? { schema: schema } : {}),
|
|
102
|
+
...(options.idField ? { idField: options.idField } : {}),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
if (options.rows)
|
|
106
|
+
void load(options.rows, options.schema).catch(failAll);
|
|
107
|
+
return {
|
|
108
|
+
getRows(req) {
|
|
109
|
+
// The signal cannot be cloned, and `parentRow` / `context` are app
|
|
110
|
+
// objects the in-memory source never reads; sending them would only
|
|
111
|
+
// risk a DataCloneError.
|
|
112
|
+
const { signal, parentRow: _parentRow, context: _context, ...plain } = req;
|
|
113
|
+
return call('getRows', [plain], signal);
|
|
114
|
+
},
|
|
115
|
+
createRow: (input) => call('createRow', [input]),
|
|
116
|
+
updateRow: (id, patch) => call('updateRow', [id, patch]),
|
|
117
|
+
deleteRow: (id) => call('deleteRow', [id]),
|
|
118
|
+
updateWhere: (filterModel, patch, selection) => call('updateWhere', [filterModel, patch, selection]),
|
|
119
|
+
getAggregate: (req) => call('getAggregate', [req]),
|
|
120
|
+
ready,
|
|
121
|
+
setRows: (rows, schema) => load(rows, schema ?? options.schema),
|
|
122
|
+
destroy() {
|
|
123
|
+
if (destroyed)
|
|
124
|
+
return;
|
|
125
|
+
destroyed = true;
|
|
126
|
+
failure = new Error('createWorkerDataSource: the source was destroyed');
|
|
127
|
+
failAll(failure);
|
|
128
|
+
worker.removeEventListener('message', onMessage);
|
|
129
|
+
worker.removeEventListener('error', onError);
|
|
130
|
+
worker.terminate?.();
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
package/dist/sheet/objects.d.ts
CHANGED
|
@@ -92,6 +92,53 @@ export declare function objectAt(objects: ReadonlyArray<SheetObject>, point: {
|
|
|
92
92
|
width: number;
|
|
93
93
|
height: number;
|
|
94
94
|
} | null): SheetObject | null;
|
|
95
|
+
/**
|
|
96
|
+
* The four panes Freeze Panes splits a sheet into: the frozen corner, the
|
|
97
|
+
* frozen rows (which scroll across), the frozen columns (which scroll
|
|
98
|
+
* down), and the body (which scrolls both ways).
|
|
99
|
+
*/
|
|
100
|
+
export type SheetPane = 'corner' | 'rows' | 'cols' | 'body';
|
|
101
|
+
/**
|
|
102
|
+
* The pane an object belongs to: the one its anchor cell is in. Excel keeps
|
|
103
|
+
* an object with that pane, so one hung from a frozen cell is drawn over
|
|
104
|
+
* the frozen cells and stays put while the sheet scrolls under it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function paneOf(anchor: {
|
|
107
|
+
row: number;
|
|
108
|
+
col: number;
|
|
109
|
+
}, freeze: {
|
|
110
|
+
rows: number;
|
|
111
|
+
cols: number;
|
|
112
|
+
}): SheetPane;
|
|
113
|
+
/**
|
|
114
|
+
* How much of an object its pane cuts off, in pixels from each edge of its
|
|
115
|
+
* box. `box` and `edges` are in the same frame: `left` measured from the
|
|
116
|
+
* inline start, `top` from the top.
|
|
117
|
+
*
|
|
118
|
+
* `edges.start` and `edges.top` are where the scrolling cells begin (past
|
|
119
|
+
* the frozen columns, or the row numbers when none are frozen; under the
|
|
120
|
+
* frozen rows, or the header), and `edges.end` and `edges.bottom` where the
|
|
121
|
+
* sheet's window ends. An object is cut on the sides its pane scrolls from,
|
|
122
|
+
* so it slides under the frozen cells, and always at the window's far
|
|
123
|
+
* edges. A frozen pane never scrolls past its start, so nothing is cut
|
|
124
|
+
* there and the object is drawn whole over its frozen cells.
|
|
125
|
+
*/
|
|
126
|
+
export declare function paneClip(pane: SheetPane, box: {
|
|
127
|
+
left: number;
|
|
128
|
+
top: number;
|
|
129
|
+
width: number;
|
|
130
|
+
height: number;
|
|
131
|
+
}, edges: {
|
|
132
|
+
start: number;
|
|
133
|
+
top: number;
|
|
134
|
+
end: number;
|
|
135
|
+
bottom: number;
|
|
136
|
+
}): {
|
|
137
|
+
start: number;
|
|
138
|
+
top: number;
|
|
139
|
+
end: number;
|
|
140
|
+
bottom: number;
|
|
141
|
+
};
|
|
95
142
|
/**
|
|
96
143
|
* The chart's spec, read from its range through `valueAt` (the computed
|
|
97
144
|
* value) and `textAt` (what the cell shows, for the labels). A chart of a
|
package/dist/sheet/objects.js
CHANGED
|
@@ -49,6 +49,39 @@ export function objectAt(objects, point, boxOf) {
|
|
|
49
49
|
}
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The pane an object belongs to: the one its anchor cell is in. Excel keeps
|
|
54
|
+
* an object with that pane, so one hung from a frozen cell is drawn over
|
|
55
|
+
* the frozen cells and stays put while the sheet scrolls under it.
|
|
56
|
+
*/
|
|
57
|
+
export function paneOf(anchor, freeze) {
|
|
58
|
+
const row = anchor.row < freeze.rows;
|
|
59
|
+
const col = anchor.col < freeze.cols;
|
|
60
|
+
return row && col ? 'corner' : row ? 'rows' : col ? 'cols' : 'body';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* How much of an object its pane cuts off, in pixels from each edge of its
|
|
64
|
+
* box. `box` and `edges` are in the same frame: `left` measured from the
|
|
65
|
+
* inline start, `top` from the top.
|
|
66
|
+
*
|
|
67
|
+
* `edges.start` and `edges.top` are where the scrolling cells begin (past
|
|
68
|
+
* the frozen columns, or the row numbers when none are frozen; under the
|
|
69
|
+
* frozen rows, or the header), and `edges.end` and `edges.bottom` where the
|
|
70
|
+
* sheet's window ends. An object is cut on the sides its pane scrolls from,
|
|
71
|
+
* so it slides under the frozen cells, and always at the window's far
|
|
72
|
+
* edges. A frozen pane never scrolls past its start, so nothing is cut
|
|
73
|
+
* there and the object is drawn whole over its frozen cells.
|
|
74
|
+
*/
|
|
75
|
+
export function paneClip(pane, box, edges) {
|
|
76
|
+
const across = pane === 'rows' || pane === 'body';
|
|
77
|
+
const down = pane === 'cols' || pane === 'body';
|
|
78
|
+
return {
|
|
79
|
+
start: across ? Math.max(0, edges.start - box.left) : 0,
|
|
80
|
+
top: down ? Math.max(0, edges.top - box.top) : 0,
|
|
81
|
+
end: Math.max(0, box.left + box.width - edges.end),
|
|
82
|
+
bottom: Math.max(0, box.top + box.height - edges.bottom),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
52
85
|
/** A cell's value as a number, or null when it is not one. */
|
|
53
86
|
function numberOf(value) {
|
|
54
87
|
if (typeof value === 'number')
|
package/dist/studio/cli.js
CHANGED
|
@@ -58,12 +58,66 @@ export async function runStudioAdd(options, io) {
|
|
|
58
58
|
const verify = await verifyScaffold(files);
|
|
59
59
|
return { schema, written, verify };
|
|
60
60
|
}
|
|
61
|
+
/** The stylesheets an `init`-generated layout imports, in cascade order. */
|
|
62
|
+
const APP_STYLESHEETS = ['app.css', 'custom.css'];
|
|
63
|
+
/**
|
|
64
|
+
* Fill the app-shell options the caller left out from the app already on disk,
|
|
65
|
+
* so `add --all` in a folder `init` generated keeps its stylesheets, title and
|
|
66
|
+
* theme. The shell layout from `init` carries no managed markers, so the merge
|
|
67
|
+
* write replaces it whole; anything it imported or named has to be carried
|
|
68
|
+
* over here or it is lost.
|
|
69
|
+
*
|
|
70
|
+
* Title: studio.config.json (shell brand, then title), else the `appTitle`
|
|
71
|
+
* constant of a layout an earlier `add --all` wrote.
|
|
72
|
+
*/
|
|
73
|
+
async function appShellDefaults(options, io) {
|
|
74
|
+
const stylesheets = options.stylesheets ?? [];
|
|
75
|
+
if (!options.stylesheets) {
|
|
76
|
+
for (const css of APP_STYLESHEETS) {
|
|
77
|
+
if ((await io.readFile(`src/${css}`)) != null)
|
|
78
|
+
stylesheets.push(css);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let title;
|
|
82
|
+
let theme;
|
|
83
|
+
const config = await io.readFile('studio.config.json');
|
|
84
|
+
if (config != null) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(config);
|
|
87
|
+
theme = parsed.theme;
|
|
88
|
+
const brand = typeof theme?.shell?.brand === 'string' ? theme.shell.brand.trim() : '';
|
|
89
|
+
title = brand || (typeof parsed.title === 'string' ? parsed.title : undefined);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* unreadable config: fall back to the layout below */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!title) {
|
|
96
|
+
const layout = await io.readFile('src/routes/+layout.svelte');
|
|
97
|
+
const m = layout?.match(/const appTitle = ("(?:[^"\\]|\\.)*")/);
|
|
98
|
+
if (m) {
|
|
99
|
+
try {
|
|
100
|
+
title = JSON.parse(m[1]);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* not a JSON string literal: keep the default */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
...options,
|
|
109
|
+
stylesheets,
|
|
110
|
+
...(options.appTitle == null && title ? { appTitle: title } : {}),
|
|
111
|
+
...(options.theme == null && typeof theme?.preset === 'string' ? { theme: theme.preset } : {}),
|
|
112
|
+
...(options.dark == null && theme?.mode === 'dark' ? { dark: true } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
61
115
|
/**
|
|
62
116
|
* Scaffold a whole multi-entity app - every entity's screen plus a nav layout
|
|
63
117
|
* and home page - and merge-write it. The `--all` path behind `svgrid-studio`.
|
|
64
118
|
*/
|
|
65
119
|
export async function runStudioAddApp(schemas, options, io) {
|
|
66
|
-
const { files } = scaffoldApp(schemas, options);
|
|
120
|
+
const { files } = scaffoldApp(schemas, await appShellDefaults(options, io));
|
|
67
121
|
const written = await writeAll(files, io);
|
|
68
122
|
const verify = await verifyScaffold(files);
|
|
69
123
|
return { written, verify };
|
|
@@ -3,7 +3,7 @@ import { blockColumns, blockStyleCss, blockClassName, sanitizeClassName, compone
|
|
|
3
3
|
import { uiComponentSpec, gridApiSettableProps, STANDARD_UI_EVENTS } from './ui-components.js';
|
|
4
4
|
import { isDarkTheme, themeTokenCss } from './themes.js';
|
|
5
5
|
import { hasFieldConditions } from '../edit-panel.js';
|
|
6
|
-
import { SVGRID_VERSION } from '../version.js';
|
|
6
|
+
import { SVGRID_VERSION, GRID_VERSION } from '../version.js';
|
|
7
7
|
import { emitEntityModules, homeFile, layoutFile, lookupVar, namesFor, prepareEntities, relationDisplayFields, sqlDdlFiles } from './emit-schema.js';
|
|
8
8
|
const has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
|
|
9
9
|
/** Per-Tabs-block active-tab state var + a stable tab id. */
|
|
@@ -5231,8 +5231,13 @@ export function runtimeDeps(project, allSource) {
|
|
|
5231
5231
|
// APIs of the SvGrid it was written by, so the app must install a runtime at
|
|
5232
5232
|
// least that new. `latest` also made a scaffold non-reproducible - an app could
|
|
5233
5233
|
// resolve a different runtime tomorrow, or break on an unrelated publish.
|
|
5234
|
-
|
|
5235
|
-
|
|
5234
|
+
// The two packages release on separate lines, so they need separate ranges.
|
|
5235
|
+
// Using this package's version for BOTH asked for `@svgrid/grid@^3.1.1`,
|
|
5236
|
+
// which does not exist, and `npm install` failed on every scaffolded app.
|
|
5237
|
+
const dependencies = {
|
|
5238
|
+
'@svgrid/grid': `^${GRID_VERSION}`,
|
|
5239
|
+
'@svgrid/enterprise': `^${SVGRID_VERSION}`,
|
|
5240
|
+
};
|
|
5236
5241
|
if (allSource.includes("from '@supabase/supabase-js'"))
|
|
5237
5242
|
dependencies['@supabase/supabase-js'] = '^2.45.0';
|
|
5238
5243
|
if (allSource.includes("import pg from 'pg'"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type StudioIO } from './cli.js';
|
|
2
2
|
import { type DbExecute, type SqlDialectName } from './introspect-db.js';
|
|
3
3
|
import { type StudioProject } from './project.js';
|
|
4
4
|
/** Terminal I/O, injected so the flow can be scripted in tests. */
|
|
@@ -30,6 +30,11 @@ export type InitFlags = {
|
|
|
30
30
|
supabaseUrl?: string;
|
|
31
31
|
/** Supabase anon key. */
|
|
32
32
|
supabaseKey?: string;
|
|
33
|
+
/**
|
|
34
|
+
* A Drizzle (.ts) or Prisma (.prisma) schema file. Skips the source question
|
|
35
|
+
* and builds the app from its tables, served from memory with sample rows.
|
|
36
|
+
*/
|
|
37
|
+
from?: string;
|
|
33
38
|
/** Pick a starter dataset by id, skipping the source question. */
|
|
34
39
|
dataset?: string;
|
|
35
40
|
/** Theme preset id. */
|
package/dist/studio/init-flow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveSchemas } from './cli.js';
|
|
1
2
|
import { crudAppFromSchemas } from './screen-suites.js';
|
|
2
3
|
import { starterDatasets, getStarterDataset } from './samples/datasets.js';
|
|
3
4
|
import { introspectDatabase, listDatabaseTables, countTableRows } from './introspect-db.js';
|
|
@@ -92,7 +93,9 @@ function unwrapRows(body) {
|
|
|
92
93
|
return [];
|
|
93
94
|
}
|
|
94
95
|
/** Step 1-2: where does the data come from, and what tables does it have? */
|
|
95
|
-
async function gatherData(flags, prompts, db, fetchText) {
|
|
96
|
+
async function gatherData(flags, prompts, db, fetchText, io) {
|
|
97
|
+
if (flags.from)
|
|
98
|
+
return gatherFromSchemaFile(flags, flags.from, prompts, io);
|
|
96
99
|
let source = 'sample';
|
|
97
100
|
if (flags.db)
|
|
98
101
|
source = 'db';
|
|
@@ -126,6 +129,44 @@ async function gatherData(flags, prompts, db, fetchText) {
|
|
|
126
129
|
prompts.say(` Using ${dataset.name} (${entities.length} tables).`);
|
|
127
130
|
return { schemas: entities, sources, seed, kind, title: dataset.name };
|
|
128
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* A schema file names the tables and columns but holds no rows and no
|
|
134
|
+
* connection, so the app runs on the in-memory source and the emitter seeds it
|
|
135
|
+
* with sample rows. `--db` + `--url` is the path to a live database.
|
|
136
|
+
*/
|
|
137
|
+
async function gatherFromSchemaFile(flags, from, prompts, io) {
|
|
138
|
+
const clash = flags.db ? '--db' : flags.supabaseUrl ? '--supabase-url' : flags.dataset ? '--dataset' : null;
|
|
139
|
+
if (clash)
|
|
140
|
+
throw new UserError(`--from and ${clash} both name the data source.`, 'Pass one of them.');
|
|
141
|
+
if ((await io.readFile(from)) == null) {
|
|
142
|
+
throw new UserError(`Schema file not found: ${from}`, 'Pass the path to a Drizzle schema (.ts) or a Prisma schema (.prisma).');
|
|
143
|
+
}
|
|
144
|
+
let all;
|
|
145
|
+
try {
|
|
146
|
+
all = await resolveSchemas(from, io);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
throw new UserError(`Could not read ${from}: ${e instanceof Error ? e.message : String(e)}`, 'Pass a Drizzle schema (.ts) or a Prisma schema (.prisma).');
|
|
150
|
+
}
|
|
151
|
+
if (all.length === 0) {
|
|
152
|
+
throw new UserError(`No tables or models found in ${from}.`, 'Drizzle tables are read from pgTable / mysqlTable / sqliteTable calls, Prisma models from `model X {` blocks.');
|
|
153
|
+
}
|
|
154
|
+
let picked = all.map((s) => s.name);
|
|
155
|
+
if (!flags.yes) {
|
|
156
|
+
prompts.say('');
|
|
157
|
+
prompts.say(`Found ${all.length} ${all.length === 1 ? 'table' : 'tables'} in ${from}:`);
|
|
158
|
+
all.forEach((s, i) => prompts.say(` ${i + 1}. ${s.name}`));
|
|
159
|
+
picked = parseSelection(await prompts.ask('Which tables? (all, or a comma list of names/numbers)', 'all'), picked);
|
|
160
|
+
}
|
|
161
|
+
const schemas = all.filter((s) => picked.includes(s.name));
|
|
162
|
+
if (schemas.length === 0)
|
|
163
|
+
throw new UserError('No tables picked - nothing to build.');
|
|
164
|
+
const sources = {};
|
|
165
|
+
for (const s of schemas)
|
|
166
|
+
sources[s.name] = { kind: 'memory' };
|
|
167
|
+
prompts.say(` Read ${schemas.length} ${schemas.length === 1 ? 'table' : 'tables'} from ${from} (in-memory data with sample rows).`);
|
|
168
|
+
return { schemas, sources, seed: {}, kind: 'memory' };
|
|
169
|
+
}
|
|
129
170
|
async function gatherFromDatabase(flags, prompts, db) {
|
|
130
171
|
if (!db)
|
|
131
172
|
throw new UserError('Connecting to a database is not available here.');
|
|
@@ -417,7 +458,7 @@ export async function runStudioInit(flags, prompts, db, io, fetchText = null) {
|
|
|
417
458
|
// it rather than as Studio just changed it.
|
|
418
459
|
const candidateDir = flags.out ?? '.';
|
|
419
460
|
const markersBefore = await markersIn(candidateDir, io);
|
|
420
|
-
const data = await gatherData(flags, prompts, db, fetchText);
|
|
461
|
+
const data = await gatherData(flags, prompts, db, fetchText, io);
|
|
421
462
|
const perEntity = await askScreens(data.schemas, flags, prompts);
|
|
422
463
|
const theme = await askTheme(flags, prompts);
|
|
423
464
|
const defaultTitle = flags.title ?? data.title ?? 'My app';
|
|
@@ -24,6 +24,12 @@ export type ScaffoldAppOptions = {
|
|
|
24
24
|
*/
|
|
25
25
|
theme?: string;
|
|
26
26
|
dark?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Global stylesheets the layout imports, as paths relative to `src/` (for
|
|
29
|
+
* example `['app.css', 'custom.css']` in an app `init` generated). List only
|
|
30
|
+
* files that exist: importing a missing file fails the build.
|
|
31
|
+
*/
|
|
32
|
+
stylesheets?: string[];
|
|
27
33
|
};
|
|
28
34
|
export declare function scaffoldApp(schemas: EntitySchema[], options?: ScaffoldAppOptions): {
|
|
29
35
|
files: GeneratedFile[];
|