vastgrid 0.1.0 → 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 +43 -18
- 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 -67
- 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 -17
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -165
- 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 -218
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -788
- 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 -698
- 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 -179
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -290
- 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 -887
- package/src/data-grid.tsx +0 -1029
- 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,214 @@
|
|
|
1
|
+
import { use, useState, useTransition } from "react";
|
|
2
|
+
import { type Aggregate, type GridCommand, MEASURES, type MeasureName, measureTakes } from "vastgrid-spec";
|
|
3
|
+
import { measureName, offers, rowColumns } from "../core/measures.js";
|
|
4
|
+
import { LocaleContext } from "./context.js";
|
|
5
|
+
import { useGridSelector, useGridStore } from "./hooks.js";
|
|
6
|
+
|
|
7
|
+
/** The measures the menu offers; `quantile` needs a `p`, which it does not ask for. */
|
|
8
|
+
const OFFERED = MEASURES.filter((fn) => fn !== "quantile");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The content of the toolbar's group menu, loaded when it first opens (§6.2 of the specification): the columns that the rows are grouped by and the measures of
|
|
12
|
+
* each group (§4.5), each change dispatched as `group.set` with the whole aggregate, or `group.clear` when nothing is
|
|
13
|
+
* left. It is there when the engine groups rows.
|
|
14
|
+
*/
|
|
15
|
+
export default function GroupPanel() {
|
|
16
|
+
const store = useGridStore();
|
|
17
|
+
const { messages } = use(LocaleContext);
|
|
18
|
+
const table = useGridSelector((snapshot) => snapshot.table);
|
|
19
|
+
const plan = useGridSelector((snapshot) => snapshot.document.plan);
|
|
20
|
+
const [fn, setFn] = useState<MeasureName>("sum");
|
|
21
|
+
const [column, setColumn] = useState("");
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
const [pending, startDispatch] = useTransition();
|
|
24
|
+
if (!table || !offers(table.capabilities, "count")) return null;
|
|
25
|
+
|
|
26
|
+
const aggregate: Aggregate = plan?.aggregate ?? { keys: [], measures: [] };
|
|
27
|
+
const rows = rowColumns(plan, table);
|
|
28
|
+
const measures = OFFERED.filter((name) => offers(table.capabilities, name));
|
|
29
|
+
const columnsFor = (name: MeasureName) => rows.filter((item) => measureTakes(name, item));
|
|
30
|
+
const target = column === "" ? undefined : rows.find((item) => item.name === column);
|
|
31
|
+
const canAdd = target ? measureTakes(fn, target) : fn === "count";
|
|
32
|
+
|
|
33
|
+
const dispatch = (command: GridCommand) =>
|
|
34
|
+
startDispatch(async () => {
|
|
35
|
+
const outcome = await store.dispatch(command);
|
|
36
|
+
setError(outcome.ok ? null : outcome.error.message);
|
|
37
|
+
});
|
|
38
|
+
const set = (next: Aggregate) =>
|
|
39
|
+
dispatch(
|
|
40
|
+
next.keys.length === 0 && next.measures.length === 0
|
|
41
|
+
? { command: "group.clear" }
|
|
42
|
+
: { command: "group.set", aggregate: next },
|
|
43
|
+
);
|
|
44
|
+
const pivots = table.capabilities.plan?.pivot !== false;
|
|
45
|
+
const pivot = aggregate.pivot?.columns ?? [];
|
|
46
|
+
// A pivot makes a column per value of its columns and measure (§4.6), so it needs a measure: rows are counted.
|
|
47
|
+
const setPivot = (columns: string[]) => {
|
|
48
|
+
const { pivot: _pivot, ...rest } = aggregate;
|
|
49
|
+
const measures =
|
|
50
|
+
columns.length > 0 && rest.measures.length === 0
|
|
51
|
+
? [{ name: measureName("count", undefined, new Set(rest.keys)), fn: "count" as const }]
|
|
52
|
+
: rest.measures;
|
|
53
|
+
set(columns.length > 0 ? { ...rest, measures, pivot: { columns } } : rest);
|
|
54
|
+
};
|
|
55
|
+
const taken = () => new Set([...aggregate.keys, ...aggregate.measures.map((measure) => measure.name)]);
|
|
56
|
+
const addMeasure = () => {
|
|
57
|
+
if (!canAdd) return;
|
|
58
|
+
const name = measureName(fn, target?.name, taken());
|
|
59
|
+
set({ ...aggregate, measures: [...aggregate.measures, { name, fn, ...(target ? { column: target.name } : {}) }] });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<fieldset className="vg-groups__part" disabled={pending}>
|
|
65
|
+
<legend>{messages.groupKeys}</legend>
|
|
66
|
+
<ul className="vg-groups__list">
|
|
67
|
+
{aggregate.keys.map((key) => (
|
|
68
|
+
<li key={key}>
|
|
69
|
+
<span>{key}</span>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
className="vg-button vg-button--small"
|
|
73
|
+
aria-label={messages.remove(key)}
|
|
74
|
+
onClick={() => set({ ...aggregate, keys: aggregate.keys.filter((other) => other !== key) })}
|
|
75
|
+
>
|
|
76
|
+
×
|
|
77
|
+
</button>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
<select
|
|
82
|
+
className="vg-input"
|
|
83
|
+
aria-label={messages.addKey}
|
|
84
|
+
value=""
|
|
85
|
+
onChange={(event) => {
|
|
86
|
+
const key = event.target.value;
|
|
87
|
+
if (!key) return;
|
|
88
|
+
// Rows grouped for the first time are counted.
|
|
89
|
+
const measures = plan?.aggregate
|
|
90
|
+
? aggregate.measures
|
|
91
|
+
: [{ name: measureName("count", undefined, new Set([key])), fn: "count" as const }];
|
|
92
|
+
set({ ...aggregate, keys: [...aggregate.keys, key], measures });
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<option value="">{messages.addKey}</option>
|
|
96
|
+
{rows
|
|
97
|
+
// Keys are the columns whose values order (§3.9), as `min` takes them.
|
|
98
|
+
.filter((item) => !aggregate.keys.includes(item.name) && measureTakes("min", item))
|
|
99
|
+
.map((item) => (
|
|
100
|
+
<option key={item.name} value={item.name}>
|
|
101
|
+
{item.name}
|
|
102
|
+
</option>
|
|
103
|
+
))}
|
|
104
|
+
</select>
|
|
105
|
+
</fieldset>
|
|
106
|
+
{pivots && (
|
|
107
|
+
<fieldset className="vg-groups__part" disabled={pending}>
|
|
108
|
+
<legend>{messages.pivotBy}</legend>
|
|
109
|
+
<ul className="vg-groups__list">
|
|
110
|
+
{pivot.map((name) => (
|
|
111
|
+
<li key={name}>
|
|
112
|
+
<span>{name}</span>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className="vg-button vg-button--small"
|
|
116
|
+
aria-label={messages.remove(name)}
|
|
117
|
+
onClick={() => setPivot(pivot.filter((other) => other !== name))}
|
|
118
|
+
>
|
|
119
|
+
×
|
|
120
|
+
</button>
|
|
121
|
+
</li>
|
|
122
|
+
))}
|
|
123
|
+
</ul>
|
|
124
|
+
<select
|
|
125
|
+
className="vg-input"
|
|
126
|
+
aria-label={messages.pivotBy}
|
|
127
|
+
value=""
|
|
128
|
+
onChange={(event) => {
|
|
129
|
+
if (event.target.value) setPivot([...pivot, event.target.value]);
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<option value="">{messages.addPivot}</option>
|
|
133
|
+
{rows
|
|
134
|
+
.filter(
|
|
135
|
+
(item) =>
|
|
136
|
+
!aggregate.keys.includes(item.name) && !pivot.includes(item.name) && measureTakes("min", item),
|
|
137
|
+
)
|
|
138
|
+
.map((item) => (
|
|
139
|
+
<option key={item.name} value={item.name}>
|
|
140
|
+
{item.name}
|
|
141
|
+
</option>
|
|
142
|
+
))}
|
|
143
|
+
</select>
|
|
144
|
+
</fieldset>
|
|
145
|
+
)}
|
|
146
|
+
<fieldset className="vg-groups__part" disabled={pending}>
|
|
147
|
+
<legend>{messages.measures}</legend>
|
|
148
|
+
<ul className="vg-groups__list">
|
|
149
|
+
{aggregate.measures.map((measure) => (
|
|
150
|
+
<li key={measure.name}>
|
|
151
|
+
<span>{measure.name}</span>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className="vg-button vg-button--small"
|
|
155
|
+
aria-label={messages.remove(measure.name)}
|
|
156
|
+
onClick={() =>
|
|
157
|
+
set({ ...aggregate, measures: aggregate.measures.filter((other) => other.name !== measure.name) })
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
×
|
|
161
|
+
</button>
|
|
162
|
+
</li>
|
|
163
|
+
))}
|
|
164
|
+
</ul>
|
|
165
|
+
<div className="vg-groups__add">
|
|
166
|
+
<select
|
|
167
|
+
className="vg-input"
|
|
168
|
+
aria-label={messages.measures}
|
|
169
|
+
value={fn}
|
|
170
|
+
onChange={(event) => setFn(event.target.value as MeasureName)}
|
|
171
|
+
>
|
|
172
|
+
{measures.map((name) => (
|
|
173
|
+
<option key={name} value={name}>
|
|
174
|
+
{messages.measureNames[name]}
|
|
175
|
+
</option>
|
|
176
|
+
))}
|
|
177
|
+
</select>
|
|
178
|
+
<select
|
|
179
|
+
className="vg-input"
|
|
180
|
+
aria-label={messages.measureColumn}
|
|
181
|
+
value={column}
|
|
182
|
+
onChange={(event) => setColumn(event.target.value)}
|
|
183
|
+
>
|
|
184
|
+
{fn === "count" && <option value="">{messages.allRows}</option>}
|
|
185
|
+
{fn !== "count" && column === "" && <option value="">{messages.measureColumn}</option>}
|
|
186
|
+
{columnsFor(fn).map((item) => (
|
|
187
|
+
<option key={item.name} value={item.name}>
|
|
188
|
+
{item.name}
|
|
189
|
+
</option>
|
|
190
|
+
))}
|
|
191
|
+
</select>
|
|
192
|
+
<button type="button" className="vg-button" disabled={!canAdd} onClick={addMeasure}>
|
|
193
|
+
{messages.addMeasure}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</fieldset>
|
|
197
|
+
{error !== null && (
|
|
198
|
+
<p role="alert" className="vg-filter__error">
|
|
199
|
+
{error}
|
|
200
|
+
</p>
|
|
201
|
+
)}
|
|
202
|
+
{plan?.aggregate && (
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
className="vg-menu__item"
|
|
206
|
+
disabled={pending}
|
|
207
|
+
onClick={() => dispatch({ command: "group.clear" })}
|
|
208
|
+
>
|
|
209
|
+
{messages.ungroup}
|
|
210
|
+
</button>
|
|
211
|
+
)}
|
|
212
|
+
</>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { use, useEffect, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import type { JsonValue } from "vastgrid-spec";
|
|
3
|
+
import { createGridStore, type GridStore, type GridStoreOptions } from "../core/store.js";
|
|
4
|
+
import type { GridSnapshot } from "../core/types.js";
|
|
5
|
+
import { GridContext } from "./context.js";
|
|
6
|
+
|
|
7
|
+
/** The store of the nearest `DataGrid` or `GridProvider`. */
|
|
8
|
+
export function useGridStore(): GridStore {
|
|
9
|
+
const store = use(GridContext);
|
|
10
|
+
if (!store) throw new Error("useGridStore needs a DataGrid or a GridProvider around it.");
|
|
11
|
+
return store;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Whether two values are the same, or objects or arrays whose members are the same (`Object.is`). */
|
|
15
|
+
export function shallowEqual<T>(a: T, b: T): boolean {
|
|
16
|
+
if (Object.is(a, b)) return true;
|
|
17
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
18
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
19
|
+
const keys = Object.keys(a);
|
|
20
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
21
|
+
return keys.every(
|
|
22
|
+
(key) =>
|
|
23
|
+
Object.hasOwn(b, key) && Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The last value a selector gave, kept while the snapshot stays or the next value is equal. */
|
|
28
|
+
class Selection<T> {
|
|
29
|
+
private snapshot: GridSnapshot | undefined;
|
|
30
|
+
private selector: ((snapshot: GridSnapshot) => T) | undefined;
|
|
31
|
+
private value: T | undefined;
|
|
32
|
+
|
|
33
|
+
read(snapshot: GridSnapshot, selector: (snapshot: GridSnapshot) => T, isEqual: (a: T, b: T) => boolean): T {
|
|
34
|
+
if (snapshot === this.snapshot && selector === this.selector) return this.value as T;
|
|
35
|
+
const next = selector(snapshot);
|
|
36
|
+
if (this.snapshot === undefined || !isEqual(this.value as T, next)) this.value = next;
|
|
37
|
+
this.snapshot = snapshot;
|
|
38
|
+
this.selector = selector;
|
|
39
|
+
return this.value as T;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A part of the store's snapshot (§5.3 of react.md). The component renders again only when the selected value
|
|
45
|
+
* changes by `isEqual` (`Object.is` by default). Selectors run on every change of the store: they are cheap and
|
|
46
|
+
* make no new objects unless `isEqual` compares them (`shallowEqual`).
|
|
47
|
+
*/
|
|
48
|
+
export function useGridSelector<T>(
|
|
49
|
+
selector: (snapshot: GridSnapshot) => T,
|
|
50
|
+
isEqual: (a: T, b: T) => boolean = Object.is,
|
|
51
|
+
): T {
|
|
52
|
+
const store = useGridStore();
|
|
53
|
+
const [selection] = useState(() => new Selection<T>());
|
|
54
|
+
const read = () => selection.read(store.getSnapshot(), selector, isEqual);
|
|
55
|
+
return useSyncExternalStore(store.subscribe, read, read);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The store's `dispatch`: the same function on every render. */
|
|
59
|
+
export function useGridDispatch(): GridStore["dispatch"] {
|
|
60
|
+
return useGridStore().dispatch;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A store for the life of the component: made once from the first options, connected while it is mounted. */
|
|
64
|
+
export function useCreateGridStore(options: GridStoreOptions): GridStore {
|
|
65
|
+
const [store] = useState(() => createGridStore(options));
|
|
66
|
+
useEffect(() => store.connect(), [store]);
|
|
67
|
+
return store;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Values of rows `[start, end)` of the view on screen, for cells that React renders (a detail panel, an overlay):
|
|
72
|
+
* one array per row, with undefined for values not read yet. Reads them when they are missing, and renders again
|
|
73
|
+
* when rows arrive.
|
|
74
|
+
*/
|
|
75
|
+
export function useGridRows(range: {
|
|
76
|
+
start: number;
|
|
77
|
+
end: number;
|
|
78
|
+
columns: readonly string[];
|
|
79
|
+
}): (JsonValue | undefined)[][] {
|
|
80
|
+
const store = useGridStore();
|
|
81
|
+
const version = useSyncExternalStore(store.subscribeRows, store.getRowsVersion, store.getRowsVersion);
|
|
82
|
+
const { start, end, columns } = range;
|
|
83
|
+
const names = columns.join("\u0000");
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
store.ensure(start, end, names === "" ? [] : names.split("\u0000"));
|
|
86
|
+
}, [store, start, end, names]);
|
|
87
|
+
return readRows(store, version, start, end, columns);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads rows of the store's cache. The cache changes inside the store, so the version of its rows is an argument:
|
|
92
|
+
* memoization (React Compiler's included) reads them again when it changes.
|
|
93
|
+
*/
|
|
94
|
+
export function readRows(
|
|
95
|
+
store: GridStore,
|
|
96
|
+
_version: number,
|
|
97
|
+
start: number,
|
|
98
|
+
end: number,
|
|
99
|
+
columns: readonly string[],
|
|
100
|
+
): (JsonValue | undefined)[][] {
|
|
101
|
+
const rows: (JsonValue | undefined)[][] = [];
|
|
102
|
+
for (let row = start; row < end; row++) rows.push(columns.map((column) => store.cell(row, column)));
|
|
103
|
+
return rows;
|
|
104
|
+
}
|