vastgrid 0.1.1 → 0.2.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 +175 -13
- package/dist/canvas/geometry.d.ts +92 -0
- package/dist/canvas/geometry.d.ts.map +1 -0
- package/dist/canvas/geometry.js +197 -0
- package/dist/canvas/geometry.js.map +1 -0
- package/dist/canvas/grid-canvas.d.ts +161 -0
- package/dist/canvas/grid-canvas.d.ts.map +1 -0
- package/dist/canvas/grid-canvas.js +1146 -0
- package/dist/canvas/grid-canvas.js.map +1 -0
- package/dist/canvas/hooks.d.ts +61 -0
- package/dist/canvas/hooks.d.ts.map +1 -0
- package/dist/canvas/hooks.js +2 -0
- package/dist/canvas/hooks.js.map +1 -0
- package/dist/canvas/keys.d.ts +26 -0
- package/dist/canvas/keys.d.ts.map +1 -0
- package/dist/canvas/keys.js +133 -0
- package/dist/canvas/keys.js.map +1 -0
- package/dist/canvas/layout.d.ts +26 -0
- package/dist/canvas/layout.d.ts.map +1 -0
- package/dist/canvas/layout.js +73 -0
- package/dist/canvas/layout.js.map +1 -0
- package/dist/canvas/scroll.d.ts +31 -0
- package/dist/canvas/scroll.d.ts.map +1 -0
- package/dist/canvas/scroll.js +111 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/text.d.ts +23 -0
- package/dist/canvas/text.d.ts.map +1 -0
- package/dist/canvas/text.js +142 -0
- package/dist/canvas/text.js.map +1 -0
- package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
- package/dist/canvas/theme.d.ts.map +1 -0
- package/dist/canvas/theme.js +62 -0
- package/dist/canvas/theme.js.map +1 -0
- package/dist/core/cache.d.ts +48 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +222 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checked.d.ts +20 -0
- package/dist/core/checked.d.ts.map +1 -0
- package/dist/core/checked.js +61 -0
- package/dist/core/checked.js.map +1 -0
- package/dist/core/client.d.ts +33 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +103 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/conditions.d.ts +48 -0
- package/dist/core/conditions.d.ts.map +1 -0
- package/dist/core/conditions.js +105 -0
- package/dist/core/conditions.js.map +1 -0
- package/dist/core/evaluate.d.ts +20 -0
- package/dist/core/evaluate.d.ts.map +1 -0
- package/dist/core/evaluate.js +210 -0
- package/dist/core/evaluate.js.map +1 -0
- package/dist/core/filters.d.ts +51 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/filters.js +303 -0
- package/dist/core/filters.js.map +1 -0
- package/dist/core/format.d.ts +14 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +303 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/measures.d.ts +26 -0
- package/dist/core/measures.d.ts.map +1 -0
- package/dist/core/measures.js +105 -0
- package/dist/core/measures.js.map +1 -0
- package/dist/core/request-ids.d.ts +20 -0
- package/dist/core/request-ids.d.ts.map +1 -0
- package/dist/core/request-ids.js +55 -0
- package/dist/core/request-ids.js.map +1 -0
- package/dist/core/rows.d.ts +79 -0
- package/dist/core/rows.d.ts.map +1 -0
- package/dist/core/rows.js +173 -0
- package/dist/core/rows.js.map +1 -0
- package/dist/core/store.d.ts +233 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +1162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/totals.d.ts +24 -0
- package/dist/core/totals.d.ts.map +1 -0
- package/dist/core/totals.js +49 -0
- package/dist/core/totals.js.map +1 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transports.d.ts +50 -0
- package/dist/core/transports.d.ts.map +1 -0
- package/dist/core/transports.js +160 -0
- package/dist/core/transports.js.map +1 -0
- package/dist/core/types.d.ts +150 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +45 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +11 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/react/accessible-rows.d.ts +13 -0
- package/dist/react/accessible-rows.d.ts.map +1 -0
- package/dist/react/accessible-rows.js +320 -0
- package/dist/react/accessible-rows.js.map +1 -0
- package/dist/react/cell-editor.d.ts +22 -0
- package/dist/react/cell-editor.d.ts.map +1 -0
- package/dist/react/cell-editor.js +431 -0
- package/dist/react/cell-editor.js.map +1 -0
- package/dist/react/cell-menu.d.ts +18 -0
- package/dist/react/cell-menu.d.ts.map +1 -0
- package/dist/react/cell-menu.js +299 -0
- package/dist/react/cell-menu.js.map +1 -0
- package/dist/react/column-menu.d.ts +16 -0
- package/dist/react/column-menu.d.ts.map +1 -0
- package/dist/react/column-menu.js +1311 -0
- package/dist/react/column-menu.js.map +1 -0
- package/dist/react/columns-menu.d.ts +7 -0
- package/dist/react/columns-menu.d.ts.map +1 -0
- package/dist/react/columns-menu.js +249 -0
- package/dist/react/columns-menu.js.map +1 -0
- package/dist/react/conditions-menu.d.ts +10 -0
- package/dist/react/conditions-menu.d.ts.map +1 -0
- package/dist/react/conditions-menu.js +795 -0
- package/dist/react/conditions-menu.js.map +1 -0
- package/dist/react/context.d.ts +29 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +18 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/data-grid.d.ts +79 -0
- package/dist/react/data-grid.d.ts.map +1 -0
- package/dist/react/data-grid.js +653 -0
- package/dist/react/data-grid.js.map +1 -0
- package/dist/react/detail-panel.d.ts +22 -0
- package/dist/react/detail-panel.d.ts.map +1 -0
- package/dist/react/detail-panel.js +242 -0
- package/dist/react/detail-panel.js.map +1 -0
- package/dist/react/export-menu.d.ts +11 -0
- package/dist/react/export-menu.d.ts.map +1 -0
- package/dist/react/export-menu.js +304 -0
- package/dist/react/export-menu.js.map +1 -0
- package/dist/react/find-bar.d.ts +11 -0
- package/dist/react/find-bar.d.ts.map +1 -0
- package/dist/react/find-bar.js +303 -0
- package/dist/react/find-bar.js.map +1 -0
- package/dist/react/format-form.d.ts +12 -0
- package/dist/react/format-form.d.ts.map +1 -0
- package/dist/react/format-form.js +863 -0
- package/dist/react/format-form.js.map +1 -0
- package/dist/react/formula-menu.d.ts +6 -0
- package/dist/react/formula-menu.d.ts.map +1 -0
- package/dist/react/formula-menu.js +343 -0
- package/dist/react/formula-menu.js.map +1 -0
- package/dist/react/grid-body.d.ts +33 -0
- package/dist/react/grid-body.d.ts.map +1 -0
- package/dist/react/grid-body.js +1389 -0
- package/dist/react/grid-body.js.map +1 -0
- package/dist/react/group-menu.d.ts +7 -0
- package/dist/react/group-menu.d.ts.map +1 -0
- package/dist/react/group-menu.js +508 -0
- package/dist/react/group-menu.js.map +1 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +161 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/messages.d.ts +180 -0
- package/dist/react/messages.d.ts.map +1 -0
- package/dist/react/messages.js +414 -0
- package/dist/react/messages.js.map +1 -0
- package/dist/react/save.d.ts +9 -0
- package/dist/react/save.d.ts.map +1 -0
- package/dist/react/save.js +102 -0
- package/dist/react/save.js.map +1 -0
- package/dist/react/static-rows.d.ts +9 -0
- package/dist/react/static-rows.d.ts.map +1 -0
- package/dist/react/static-rows.js +176 -0
- package/dist/react/static-rows.js.map +1 -0
- package/dist/react/status-bar.d.ts +12 -0
- package/dist/react/status-bar.d.ts.map +1 -0
- package/dist/react/status-bar.js +413 -0
- package/dist/react/status-bar.js.map +1 -0
- package/dist/react/toolbar-popover.d.ts +14 -0
- package/dist/react/toolbar-popover.d.ts.map +1 -0
- package/dist/react/toolbar-popover.js +143 -0
- package/dist/react/toolbar-popover.js.map +1 -0
- package/dist/react/toolbar.d.ts +13 -0
- package/dist/react/toolbar.d.ts.map +1 -0
- package/dist/react/toolbar.js +344 -0
- package/dist/react/toolbar.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +496 -316
- package/package.json +42 -17
- package/src/canvas/geometry.ts +197 -0
- package/src/canvas/grid-canvas.ts +1222 -0
- package/src/canvas/hooks.ts +61 -0
- package/src/canvas/keys.ts +96 -0
- package/src/canvas/layout.ts +77 -0
- package/src/canvas/scroll.ts +120 -0
- package/src/canvas/text.ts +155 -0
- package/src/{theme.ts → canvas/theme.ts} +14 -1
- package/src/core/cache.ts +247 -0
- package/src/core/checked.ts +64 -0
- package/src/core/client.ts +103 -0
- package/src/core/conditions.ts +89 -0
- package/src/core/evaluate.ts +221 -0
- package/src/core/filters.ts +268 -0
- package/src/core/format.ts +309 -0
- package/src/core/measures.ts +99 -0
- package/src/core/request-ids.ts +54 -0
- package/src/core/rows.ts +213 -0
- package/src/core/store.ts +1122 -0
- package/src/core/totals.ts +59 -0
- package/src/core/trace.ts +44 -0
- package/src/core/transports.ts +177 -0
- package/src/core/types.ts +168 -0
- package/src/core.ts +39 -0
- package/src/index.ts +24 -12
- package/src/react/accessible-rows.tsx +242 -0
- package/src/react/cell-editor.tsx +216 -0
- package/src/react/cell-menu.tsx +166 -0
- package/src/react/column-menu.tsx +619 -0
- package/src/react/columns-menu.tsx +116 -0
- package/src/react/conditions-menu.tsx +289 -0
- package/src/react/context.ts +33 -0
- package/src/react/data-grid.tsx +286 -0
- package/src/react/detail-panel.tsx +92 -0
- package/src/react/export-menu.tsx +112 -0
- package/src/react/find-bar.tsx +112 -0
- package/src/react/format-form.tsx +421 -0
- package/src/react/formula-menu.tsx +163 -0
- package/src/react/grid-body.tsx +744 -0
- package/src/react/group-menu.tsx +214 -0
- package/src/react/hooks.ts +104 -0
- package/src/react/messages.ts +556 -0
- package/src/react/save.ts +42 -0
- package/src/react/static-rows.tsx +59 -0
- package/src/react/status-bar.tsx +175 -0
- package/src/react/toolbar-popover.tsx +69 -0
- package/src/react/toolbar.tsx +152 -0
- package/src/server.ts +58 -0
- package/src/styles.css +496 -316
- package/dist/components/bars.d.ts +0 -14
- package/dist/components/bars.d.ts.map +0 -1
- package/dist/components/bars.js +0 -68
- package/dist/components/bars.js.map +0 -1
- package/dist/components/cell-inspector.d.ts +0 -11
- package/dist/components/cell-inspector.d.ts.map +0 -1
- package/dist/components/cell-inspector.js +0 -37
- package/dist/components/cell-inspector.js.map +0 -1
- package/dist/components/columns-panel.d.ts +0 -12
- package/dist/components/columns-panel.d.ts.map +0 -1
- package/dist/components/columns-panel.js +0 -11
- package/dist/components/columns-panel.js.map +0 -1
- package/dist/components/filter-panel.d.ts +0 -22
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -169
- package/dist/components/filter-panel.js.map +0 -1
- package/dist/components/find-bar.d.ts +0 -9
- package/dist/components/find-bar.d.ts.map +0 -1
- package/dist/components/find-bar.js +0 -46
- package/dist/components/find-bar.js.map +0 -1
- package/dist/components/popover.d.ts +0 -38
- package/dist/components/popover.d.ts.map +0 -1
- package/dist/components/popover.js +0 -104
- package/dist/components/popover.js.map +0 -1
- package/dist/components/stats-panel.d.ts +0 -12
- package/dist/components/stats-panel.d.ts.map +0 -1
- package/dist/components/stats-panel.js +0 -40
- package/dist/components/stats-panel.js.map +0 -1
- package/dist/controller.d.ts +0 -227
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -845
- package/dist/controller.js.map +0 -1
- package/dist/data-grid.d.ts +0 -51
- package/dist/data-grid.d.ts.map +0 -1
- package/dist/data-grid.js +0 -704
- package/dist/data-grid.js.map +0 -1
- package/dist/labels.d.ts +0 -9
- package/dist/labels.d.ts.map +0 -1
- package/dist/labels.js +0 -38
- package/dist/labels.js.map +0 -1
- package/dist/messages.d.ts +0 -99
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js +0 -276
- package/dist/messages.js.map +0 -1
- package/dist/renderer.d.ts +0 -52
- package/dist/renderer.d.ts.map +0 -1
- package/dist/renderer.js +0 -522
- package/dist/renderer.js.map +0 -1
- package/dist/text.d.ts +0 -14
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js +0 -63
- package/dist/text.js.map +0 -1
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -54
- package/dist/theme.js.map +0 -1
- package/src/components/bars.tsx +0 -180
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -293
- package/src/components/find-bar.tsx +0 -104
- package/src/components/popover.tsx +0 -194
- package/src/components/stats-panel.tsx +0 -153
- package/src/controller.ts +0 -945
- package/src/data-grid.tsx +0 -1035
- package/src/labels.ts +0 -46
- package/src/messages.ts +0 -390
- package/src/renderer.ts +0 -535
- package/src/text.ts +0 -55
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { use, useEffect, useState } from "react";
|
|
2
|
+
import type { CellRange } from "vastgrid-spec";
|
|
3
|
+
import { checkedCount } from "../core/checked.js";
|
|
4
|
+
import type { GridStore } from "../core/store.js";
|
|
5
|
+
import { LocaleContext, SaveContext } from "./context.js";
|
|
6
|
+
import { useGridSelector, useGridStore } from "./hooks.js";
|
|
7
|
+
|
|
8
|
+
interface Summary {
|
|
9
|
+
count: number;
|
|
10
|
+
numericCount: number;
|
|
11
|
+
sum: unknown;
|
|
12
|
+
average: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** How long the selection must stay before it is summed. */
|
|
16
|
+
const SUMMARY_DELAY = 150;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The count and sum of a selection of one range of several cells (`summarize`, §8.2), read once the selection has
|
|
20
|
+
* stayed for a moment, so that holding a key or dragging never waits for it; null otherwise.
|
|
21
|
+
*/
|
|
22
|
+
function useSummary(store: GridStore, selection: readonly CellRange[] | undefined): Summary | null {
|
|
23
|
+
const [result, setResult] = useState<{ selection: readonly CellRange[]; summary: Summary } | null>(null);
|
|
24
|
+
const summarizes = useGridSelector((snapshot) => snapshot.table?.capabilities.summarize === true);
|
|
25
|
+
const range = selection?.length === 1 ? selection[0] : undefined;
|
|
26
|
+
const several =
|
|
27
|
+
range !== undefined &&
|
|
28
|
+
(range.rows === "all" || range.rows[1] - range.rows[0] > 1 || range.columns === "all" || range.columns.length > 1);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!summarizes || !several || !selection || !range) return;
|
|
31
|
+
const abort = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
const snapshot = store.getSnapshot();
|
|
34
|
+
const rowCount = snapshot.view?.rowCount ?? 0;
|
|
35
|
+
const rows: [number, number] = range.rows === "all" ? [0, rowCount] : [range.rows[0], range.rows[1]];
|
|
36
|
+
const columns = range.columns === "all" ? snapshot.displayed.map((item) => item.column.name) : range.columns;
|
|
37
|
+
store
|
|
38
|
+
.summarize([rows], columns, abort.signal)
|
|
39
|
+
.then((summary) => setResult({ selection, summary }))
|
|
40
|
+
.catch(() => {});
|
|
41
|
+
}, SUMMARY_DELAY);
|
|
42
|
+
return () => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
abort.abort();
|
|
45
|
+
};
|
|
46
|
+
}, [store, summarizes, several, selection, range]);
|
|
47
|
+
return result && result.selection === selection ? result.summary : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function format(number: Intl.NumberFormat, value: unknown) {
|
|
51
|
+
if (typeof value === "number") return number.format(value);
|
|
52
|
+
return typeof value === "string" ? value : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface StatusBarProps {
|
|
56
|
+
announce: (text: string) => void;
|
|
57
|
+
/** Gives the focus back to the grid, once a button that had it is gone. */
|
|
58
|
+
onDone: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The last write that failed (§6.6 of react.md): why, with Retry (Overwrite after a conflict) while the table
|
|
63
|
+
* has the revision its rows were read at, and Discard. Both give the focus back to the grid.
|
|
64
|
+
*/
|
|
65
|
+
function FailedWrite({ announce, onDone }: StatusBarProps) {
|
|
66
|
+
const store = useGridStore();
|
|
67
|
+
const { messages } = use(LocaleContext);
|
|
68
|
+
const failed = useGridSelector((snapshot) => snapshot.failedWrite);
|
|
69
|
+
const revision = useGridSelector((snapshot) => snapshot.table?.revision);
|
|
70
|
+
if (!failed) return null;
|
|
71
|
+
const retry = failed.revision === revision ? failed.retry : null;
|
|
72
|
+
const text =
|
|
73
|
+
failed.error.code === "revision_changed" || (failed.retry !== null && retry === null)
|
|
74
|
+
? messages.writeRevisionChanged
|
|
75
|
+
: failed.error.code === "edit_conflict"
|
|
76
|
+
? messages.writeConflict
|
|
77
|
+
: messages.writeFailed(failed.error.message);
|
|
78
|
+
const send = () => {
|
|
79
|
+
onDone();
|
|
80
|
+
void store.retryWrite().then((outcome) => {
|
|
81
|
+
if (!outcome.ok && !store.getSnapshot().failedWrite) announce(messages.writeFailed(outcome.error.message));
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
return (
|
|
85
|
+
<span className="vg-statusbar__failed" role="alert">
|
|
86
|
+
<span className="vg-statusbar__error">{text}</span>
|
|
87
|
+
{retry && (
|
|
88
|
+
<button type="button" className="vg-button vg-button--small" onClick={send}>
|
|
89
|
+
{retry === "overwrite" ? messages.overwrite : messages.retry}
|
|
90
|
+
</button>
|
|
91
|
+
)}
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="vg-button vg-button--small"
|
|
95
|
+
onClick={() => {
|
|
96
|
+
onDone();
|
|
97
|
+
store.discardWrite();
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{messages.discard}
|
|
101
|
+
</button>
|
|
102
|
+
</span>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The status bar: the active cell, the sum of the selection, what the grid is doing (saving and the outcome of the
|
|
108
|
+
* last save included) or that the file changed on disk, a write that failed, the cells not saved yet and the rows.
|
|
109
|
+
*/
|
|
110
|
+
export function StatusBar({ announce, onDone }: StatusBarProps) {
|
|
111
|
+
const store = useGridStore();
|
|
112
|
+
const { messages, number } = use(LocaleContext);
|
|
113
|
+
const saving = use(SaveContext);
|
|
114
|
+
const saves = useGridSelector((snapshot) => snapshot.table?.capabilities.save === true);
|
|
115
|
+
const edits = useGridSelector((snapshot) => snapshot.table?.edits);
|
|
116
|
+
const revision = useGridSelector((snapshot) => snapshot.table?.revision);
|
|
117
|
+
const fileChanged = useGridSelector((snapshot) => snapshot.table?.fileChanged === true);
|
|
118
|
+
const rowCount = useGridSelector((snapshot) => snapshot.view?.rowCount);
|
|
119
|
+
const building = useGridSelector((snapshot) => snapshot.view?.status === "building");
|
|
120
|
+
const pending = useGridSelector((snapshot) => snapshot.pending !== null);
|
|
121
|
+
const failedWrite = useGridSelector((snapshot) => snapshot.failedWrite !== null);
|
|
122
|
+
const error = useGridSelector((snapshot) => snapshot.error);
|
|
123
|
+
const active = useGridSelector((snapshot) => snapshot.session.active);
|
|
124
|
+
const selection = useGridSelector((snapshot) => snapshot.session.selection);
|
|
125
|
+
const summary = useSummary(store, selection);
|
|
126
|
+
const checked = useGridSelector((snapshot) =>
|
|
127
|
+
checkedCount(snapshot.session.checked, snapshot.document.plan, snapshot.view?.rowCount ?? 0),
|
|
128
|
+
);
|
|
129
|
+
const doing = error
|
|
130
|
+
? messages.failed(error.message)
|
|
131
|
+
: rowCount === undefined
|
|
132
|
+
? messages.loading
|
|
133
|
+
: saving?.pending
|
|
134
|
+
? messages.saving
|
|
135
|
+
: saving?.outcome && edits && revision === saving.outcome.revision && edits.version <= saving.outcome.version
|
|
136
|
+
? saving.outcome.text
|
|
137
|
+
: fileChanged
|
|
138
|
+
? messages.changedOnDisk
|
|
139
|
+
: pending || building
|
|
140
|
+
? messages.reading
|
|
141
|
+
: "";
|
|
142
|
+
return (
|
|
143
|
+
<div className="vg-statusbar">
|
|
144
|
+
<span className="vg-statusbar__cell">
|
|
145
|
+
{active && rowCount !== undefined && rowCount > 0
|
|
146
|
+
? messages.cell(number.format(active.row + 1), active.column)
|
|
147
|
+
: ""}
|
|
148
|
+
</span>
|
|
149
|
+
<span className="vg-toolbar__spacer" />
|
|
150
|
+
{summary && (
|
|
151
|
+
<span className="vg-statusbar__summary">
|
|
152
|
+
<span>{messages.count(number.format(summary.count))}</span>
|
|
153
|
+
{summary.numericCount > 0 && <span>{messages.sum(format(number, summary.sum))}</span>}
|
|
154
|
+
{summary.numericCount > 0 && <span>{messages.average(format(number, summary.average))}</span>}
|
|
155
|
+
</span>
|
|
156
|
+
)}
|
|
157
|
+
{failedWrite ? (
|
|
158
|
+
<FailedWrite announce={announce} onDone={onDone} />
|
|
159
|
+
) : (
|
|
160
|
+
<span className="vg-statusbar__muted" role="status">
|
|
161
|
+
{doing}
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
{checked !== 0 && (
|
|
165
|
+
<span className="vg-statusbar__checked">
|
|
166
|
+
{checked === null ? messages.checkedUnknown : messages.checkedRows(number.format(checked))}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
{saves && edits && edits.cells > 0 && (
|
|
170
|
+
<span className="vg-statusbar__unsaved">{messages.unsaved(number.format(edits.cells))}</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="vg-toolbar__count">{rowCount === undefined ? "" : messages.rows(number.format(rowCount))}</span>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ReactNode, Suspense, type ToggleEvent, use, useEffect, useId, useRef, useState } from "react";
|
|
2
|
+
import { LocaleContext } from "./context.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A button of the toolbar and its popover, whose content is rendered only while it is open, so that a content loaded
|
|
6
|
+
* with `lazy` is downloaded when it is first opened (§10 of react.md) and starts from the state as it is then. The
|
|
7
|
+
* popover opens under the button, its right edge at the button's, and follows its content's size.
|
|
8
|
+
*/
|
|
9
|
+
export function ToolbarPopover({
|
|
10
|
+
label,
|
|
11
|
+
title,
|
|
12
|
+
className,
|
|
13
|
+
pressed,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
title: string;
|
|
18
|
+
className: string;
|
|
19
|
+
pressed?: boolean;
|
|
20
|
+
children: (close: () => void) => ReactNode;
|
|
21
|
+
}) {
|
|
22
|
+
const { messages } = use(LocaleContext);
|
|
23
|
+
const [open, setOpen] = useState(false);
|
|
24
|
+
const id = useId();
|
|
25
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
26
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const menu = menuRef.current;
|
|
30
|
+
if (!open || !menu) return;
|
|
31
|
+
const place = () => {
|
|
32
|
+
const button = buttonRef.current;
|
|
33
|
+
if (!button) return;
|
|
34
|
+
const rect = button.getBoundingClientRect();
|
|
35
|
+
menu.style.top = `${rect.bottom + 4}px`;
|
|
36
|
+
menu.style.left = `${Math.max(8, rect.right - menu.offsetWidth)}px`;
|
|
37
|
+
};
|
|
38
|
+
place();
|
|
39
|
+
// The content may arrive after the popover opened.
|
|
40
|
+
const observer = new ResizeObserver(place);
|
|
41
|
+
observer.observe(menu);
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}, [open]);
|
|
44
|
+
|
|
45
|
+
const onToggle = (event: ToggleEvent<HTMLDivElement>) => setOpen(event.newState === "open");
|
|
46
|
+
// By id, not by the ref: the content gets it during render.
|
|
47
|
+
const close = () => document.getElementById(id)?.hidePopover();
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<button ref={buttonRef} type="button" className="vg-button" popoverTarget={id} aria-pressed={pressed}>
|
|
52
|
+
{label}
|
|
53
|
+
</button>
|
|
54
|
+
<div
|
|
55
|
+
ref={menuRef}
|
|
56
|
+
id={id}
|
|
57
|
+
popover="auto"
|
|
58
|
+
role="dialog"
|
|
59
|
+
className={`vg-menu ${className}`}
|
|
60
|
+
aria-label={title}
|
|
61
|
+
onToggle={onToggle}
|
|
62
|
+
>
|
|
63
|
+
{open && (
|
|
64
|
+
<Suspense fallback={<p className="vg-filter__note">{messages.loading}</p>}>{children(close)}</Suspense>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { lazy, use, useDeferredValue, useEffect, useState, useTransition } from "react";
|
|
2
|
+
import { type Expr, partsOf } from "vastgrid-spec";
|
|
3
|
+
import { isApple } from "../canvas/keys.js";
|
|
4
|
+
import { offers } from "../core/measures.js";
|
|
5
|
+
import { LocaleContext, SaveContext } from "./context.js";
|
|
6
|
+
import { ExportMenu } from "./export-menu.js";
|
|
7
|
+
import { useGridSelector, useGridStore } from "./hooks.js";
|
|
8
|
+
import { ToolbarPopover } from "./toolbar-popover.js";
|
|
9
|
+
|
|
10
|
+
// The menus load when they first open (§10 of react.md).
|
|
11
|
+
const ConditionsPanel = lazy(() => import("./conditions-menu.js"));
|
|
12
|
+
const ColumnsPanel = lazy(() => import("./columns-menu.js"));
|
|
13
|
+
const GroupPanel = lazy(() => import("./group-menu.js"));
|
|
14
|
+
const FormulaPanel = lazy(() => import("./formula-menu.js"));
|
|
15
|
+
|
|
16
|
+
/** How long typing must pause before the search is set. */
|
|
17
|
+
const SEARCH_DELAY = 200;
|
|
18
|
+
|
|
19
|
+
/** The text of the search part of a plan's `where` (§6.2.1). */
|
|
20
|
+
function searchOf(where: Expr | undefined): string {
|
|
21
|
+
for (const part of partsOf(where)) if ("search" in part) return part.search;
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The toolbar: a search box when the engine searches (§3.5), the conditions menu when it filters (§6.2.1), the
|
|
27
|
+
* columns menu (§6.3), the group menu when it groups rows (§4.5), the formula
|
|
28
|
+
* menu when it derives columns (§4.11), a
|
|
29
|
+
* button that saves the written cells when the engine saves (§7.5), and one that reads the file again when it
|
|
30
|
+
* changed on disk while cells were written (§7.6). The
|
|
31
|
+
* search's text is React state, which updates as the person types; `search.set` follows through
|
|
32
|
+
* `useDeferredValue` once typing pauses.
|
|
33
|
+
*/
|
|
34
|
+
export function Toolbar({ announce }: { announce: (text: string) => void }) {
|
|
35
|
+
const store = useGridStore();
|
|
36
|
+
const { messages } = use(LocaleContext);
|
|
37
|
+
const fileChanged = useGridSelector((snapshot) => snapshot.table?.fileChanged === true);
|
|
38
|
+
const [reloading, startReload] = useTransition();
|
|
39
|
+
const reload = () =>
|
|
40
|
+
startReload(async () => {
|
|
41
|
+
try {
|
|
42
|
+
await store.reload();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
announce(messages.failed(error instanceof Error ? error.message : String(error)));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const searches = useGridSelector(
|
|
48
|
+
(snapshot) => snapshot.table !== null && snapshot.table.capabilities.plan?.search !== false,
|
|
49
|
+
);
|
|
50
|
+
const saving = use(SaveContext);
|
|
51
|
+
const saves = useGridSelector((snapshot) => snapshot.table?.capabilities.save === true);
|
|
52
|
+
const unsaved = useGridSelector((snapshot) => snapshot.table?.edits.cells ?? 0);
|
|
53
|
+
const current = useGridSelector((snapshot) => searchOf(snapshot.document.plan?.where));
|
|
54
|
+
const [text, setText] = useState(current);
|
|
55
|
+
const [shown, setShown] = useState(current);
|
|
56
|
+
// A search set elsewhere (an agent, a saved view) replaces the text.
|
|
57
|
+
if (shown !== current) {
|
|
58
|
+
setShown(current);
|
|
59
|
+
setText(current);
|
|
60
|
+
}
|
|
61
|
+
const deferred = useDeferredValue(text);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (deferred === current) return;
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
void store.dispatch({ command: "search.set", text: deferred });
|
|
66
|
+
}, SEARCH_DELAY);
|
|
67
|
+
return () => clearTimeout(timer);
|
|
68
|
+
}, [store, deferred, current]);
|
|
69
|
+
const showsSave = saves && saving !== null;
|
|
70
|
+
const exports = useGridSelector((snapshot) => (snapshot.table?.capabilities.export?.formats.length ?? 0) > 0);
|
|
71
|
+
const derives = useGridSelector(
|
|
72
|
+
(snapshot) => snapshot.table !== null && snapshot.table.capabilities.plan?.derive !== false,
|
|
73
|
+
);
|
|
74
|
+
const groups = useGridSelector((snapshot) => snapshot.table !== null && offers(snapshot.table.capabilities, "count"));
|
|
75
|
+
const grouped = useGridSelector((snapshot) => snapshot.document.plan?.aggregate !== undefined);
|
|
76
|
+
const filters = useGridSelector(
|
|
77
|
+
(snapshot) => snapshot.table !== null && snapshot.table.capabilities.plan?.where !== false,
|
|
78
|
+
);
|
|
79
|
+
const conditions = useGridSelector(
|
|
80
|
+
(snapshot) => partsOf(snapshot.document.plan?.where).filter((part) => !("search" in part)).length,
|
|
81
|
+
);
|
|
82
|
+
const opened = useGridSelector((snapshot) => snapshot.view !== null);
|
|
83
|
+
const detail = useGridSelector((snapshot) => snapshot.session.detail?.open === true);
|
|
84
|
+
if (!searches && !showsSave && !fileChanged && !exports && !derives && !groups && !opened) return null;
|
|
85
|
+
return (
|
|
86
|
+
<div className="vg-toolbar">
|
|
87
|
+
{searches && (
|
|
88
|
+
<input
|
|
89
|
+
type="search"
|
|
90
|
+
className="vg-input vg-toolbar__search"
|
|
91
|
+
aria-label={messages.search}
|
|
92
|
+
placeholder={messages.search}
|
|
93
|
+
value={text}
|
|
94
|
+
onChange={(event) => setText(event.target.value)}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
<span className="vg-toolbar__spacer" />
|
|
98
|
+
{fileChanged && (
|
|
99
|
+
<button type="button" className="vg-button" disabled={reloading} onClick={reload}>
|
|
100
|
+
{unsaved > 0 ? messages.discardAndReload : messages.reload}
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
103
|
+
{opened && (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className="vg-button"
|
|
107
|
+
aria-pressed={detail}
|
|
108
|
+
onClick={() => void store.dispatch({ command: detail ? "detail.close" : "detail.open" })}
|
|
109
|
+
>
|
|
110
|
+
{messages.details}
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
113
|
+
{filters && (
|
|
114
|
+
<ToolbarPopover
|
|
115
|
+
label={conditions > 0 ? messages.conditionsCount(String(conditions)) : messages.conditions}
|
|
116
|
+
title={messages.conditionsTitle}
|
|
117
|
+
className="vg-conditions"
|
|
118
|
+
pressed={conditions > 0}
|
|
119
|
+
>
|
|
120
|
+
{(close) => <ConditionsPanel close={close} />}
|
|
121
|
+
</ToolbarPopover>
|
|
122
|
+
)}
|
|
123
|
+
{opened && (
|
|
124
|
+
<ToolbarPopover label={messages.columns} title={messages.columns} className="vg-columns">
|
|
125
|
+
{() => <ColumnsPanel />}
|
|
126
|
+
</ToolbarPopover>
|
|
127
|
+
)}
|
|
128
|
+
{groups && (
|
|
129
|
+
<ToolbarPopover label={messages.groups} title={messages.groups} className="vg-groups" pressed={grouped}>
|
|
130
|
+
{() => <GroupPanel />}
|
|
131
|
+
</ToolbarPopover>
|
|
132
|
+
)}
|
|
133
|
+
{derives && (
|
|
134
|
+
<ToolbarPopover label={messages.formulas} title={messages.formulas} className="vg-formulas">
|
|
135
|
+
{() => <FormulaPanel />}
|
|
136
|
+
</ToolbarPopover>
|
|
137
|
+
)}
|
|
138
|
+
<ExportMenu announce={announce} />
|
|
139
|
+
{showsSave && (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className="vg-button"
|
|
143
|
+
aria-keyshortcuts={isApple() ? "Meta+S" : "Control+S"}
|
|
144
|
+
disabled={unsaved === 0 || saving.pending}
|
|
145
|
+
onClick={saving.save}
|
|
146
|
+
>
|
|
147
|
+
{messages.save}
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Server Component or a server render calls (§7 of react.md). Imports no React.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
checkPlan,
|
|
6
|
+
displayedColumns,
|
|
7
|
+
emptyDocument,
|
|
8
|
+
type GridDocument,
|
|
9
|
+
planOptions,
|
|
10
|
+
repairDocument,
|
|
11
|
+
} from "vastgrid-spec";
|
|
12
|
+
import { GridClient, type Transport } from "./core/client.js";
|
|
13
|
+
import type { InitialView, OpenOptions, RowsResult, TableInfo } from "./core/types.js";
|
|
14
|
+
|
|
15
|
+
export interface ReadInitialOptions {
|
|
16
|
+
/** The `ref` of the table to open (§8.2). */
|
|
17
|
+
table: string;
|
|
18
|
+
open?: OpenOptions;
|
|
19
|
+
/** The document the grid starts with; its plan is the view read. */
|
|
20
|
+
document?: GridDocument;
|
|
21
|
+
/** Rows to read from the start of the view; 100 by default. */
|
|
22
|
+
rows?: number;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads the table, the view of a document and its first rows, for `initial` (§7 of react.md): give the result,
|
|
28
|
+
* or the promise of it, to `DataGrid`. Opens the table and closes it again.
|
|
29
|
+
*/
|
|
30
|
+
export async function readInitialView(transport: Transport, options: ReadInitialOptions): Promise<InitialView> {
|
|
31
|
+
const client = new GridClient(transport);
|
|
32
|
+
const info = await client.request<TableInfo>(
|
|
33
|
+
"open",
|
|
34
|
+
{ ref: options.table, ...(options.open ? { read: options.open } : {}) },
|
|
35
|
+
options.signal,
|
|
36
|
+
);
|
|
37
|
+
try {
|
|
38
|
+
const document = repairDocument(
|
|
39
|
+
options.document ?? emptyDocument(),
|
|
40
|
+
info.columns,
|
|
41
|
+
planOptions(info.capabilities),
|
|
42
|
+
).document;
|
|
43
|
+
const plan = document.plan ?? {};
|
|
44
|
+
const view = checkPlan(plan, info.columns, planOptions(info.capabilities));
|
|
45
|
+
const columns = displayedColumns(document, view.columns).map((item) => item.column.name);
|
|
46
|
+
if (document.rowKey !== undefined && !columns.includes(document.rowKey)) columns.push(document.rowKey);
|
|
47
|
+
const wanted = options.rows ?? 100;
|
|
48
|
+
const end = info.rowCount === null ? wanted : Math.min(wanted, info.rowCount);
|
|
49
|
+
const rows = await client.request<RowsResult>(
|
|
50
|
+
"rows",
|
|
51
|
+
{ table: info.table, revision: info.revision, plan, start: 0, end: Math.max(end, 1), columns },
|
|
52
|
+
options.signal,
|
|
53
|
+
);
|
|
54
|
+
return { table: info, plan, rows };
|
|
55
|
+
} finally {
|
|
56
|
+
await client.request("close", { table: info.table }).catch(() => {});
|
|
57
|
+
}
|
|
58
|
+
}
|