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.
Files changed (312) hide show
  1. package/README.md +175 -13
  2. package/dist/canvas/geometry.d.ts +92 -0
  3. package/dist/canvas/geometry.d.ts.map +1 -0
  4. package/dist/canvas/geometry.js +197 -0
  5. package/dist/canvas/geometry.js.map +1 -0
  6. package/dist/canvas/grid-canvas.d.ts +161 -0
  7. package/dist/canvas/grid-canvas.d.ts.map +1 -0
  8. package/dist/canvas/grid-canvas.js +1146 -0
  9. package/dist/canvas/grid-canvas.js.map +1 -0
  10. package/dist/canvas/hooks.d.ts +61 -0
  11. package/dist/canvas/hooks.d.ts.map +1 -0
  12. package/dist/canvas/hooks.js +2 -0
  13. package/dist/canvas/hooks.js.map +1 -0
  14. package/dist/canvas/keys.d.ts +26 -0
  15. package/dist/canvas/keys.d.ts.map +1 -0
  16. package/dist/canvas/keys.js +133 -0
  17. package/dist/canvas/keys.js.map +1 -0
  18. package/dist/canvas/layout.d.ts +26 -0
  19. package/dist/canvas/layout.d.ts.map +1 -0
  20. package/dist/canvas/layout.js +73 -0
  21. package/dist/canvas/layout.js.map +1 -0
  22. package/dist/canvas/scroll.d.ts +31 -0
  23. package/dist/canvas/scroll.d.ts.map +1 -0
  24. package/dist/canvas/scroll.js +111 -0
  25. package/dist/canvas/scroll.js.map +1 -0
  26. package/dist/canvas/text.d.ts +23 -0
  27. package/dist/canvas/text.d.ts.map +1 -0
  28. package/dist/canvas/text.js +142 -0
  29. package/dist/canvas/text.js.map +1 -0
  30. package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
  31. package/dist/canvas/theme.d.ts.map +1 -0
  32. package/dist/canvas/theme.js +62 -0
  33. package/dist/canvas/theme.js.map +1 -0
  34. package/dist/core/cache.d.ts +48 -0
  35. package/dist/core/cache.d.ts.map +1 -0
  36. package/dist/core/cache.js +222 -0
  37. package/dist/core/cache.js.map +1 -0
  38. package/dist/core/checked.d.ts +20 -0
  39. package/dist/core/checked.d.ts.map +1 -0
  40. package/dist/core/checked.js +61 -0
  41. package/dist/core/checked.js.map +1 -0
  42. package/dist/core/client.d.ts +33 -0
  43. package/dist/core/client.d.ts.map +1 -0
  44. package/dist/core/client.js +103 -0
  45. package/dist/core/client.js.map +1 -0
  46. package/dist/core/conditions.d.ts +48 -0
  47. package/dist/core/conditions.d.ts.map +1 -0
  48. package/dist/core/conditions.js +105 -0
  49. package/dist/core/conditions.js.map +1 -0
  50. package/dist/core/evaluate.d.ts +20 -0
  51. package/dist/core/evaluate.d.ts.map +1 -0
  52. package/dist/core/evaluate.js +210 -0
  53. package/dist/core/evaluate.js.map +1 -0
  54. package/dist/core/filters.d.ts +51 -0
  55. package/dist/core/filters.d.ts.map +1 -0
  56. package/dist/core/filters.js +303 -0
  57. package/dist/core/filters.js.map +1 -0
  58. package/dist/core/format.d.ts +14 -0
  59. package/dist/core/format.d.ts.map +1 -0
  60. package/dist/core/format.js +303 -0
  61. package/dist/core/format.js.map +1 -0
  62. package/dist/core/measures.d.ts +26 -0
  63. package/dist/core/measures.d.ts.map +1 -0
  64. package/dist/core/measures.js +105 -0
  65. package/dist/core/measures.js.map +1 -0
  66. package/dist/core/request-ids.d.ts +20 -0
  67. package/dist/core/request-ids.d.ts.map +1 -0
  68. package/dist/core/request-ids.js +55 -0
  69. package/dist/core/request-ids.js.map +1 -0
  70. package/dist/core/rows.d.ts +79 -0
  71. package/dist/core/rows.d.ts.map +1 -0
  72. package/dist/core/rows.js +173 -0
  73. package/dist/core/rows.js.map +1 -0
  74. package/dist/core/store.d.ts +233 -0
  75. package/dist/core/store.d.ts.map +1 -0
  76. package/dist/core/store.js +1162 -0
  77. package/dist/core/store.js.map +1 -0
  78. package/dist/core/totals.d.ts +24 -0
  79. package/dist/core/totals.d.ts.map +1 -0
  80. package/dist/core/totals.js +49 -0
  81. package/dist/core/totals.js.map +1 -0
  82. package/dist/core/trace.d.ts +7 -0
  83. package/dist/core/trace.d.ts.map +1 -0
  84. package/dist/core/trace.js +26 -0
  85. package/dist/core/trace.js.map +1 -0
  86. package/dist/core/transports.d.ts +50 -0
  87. package/dist/core/transports.d.ts.map +1 -0
  88. package/dist/core/transports.js +160 -0
  89. package/dist/core/transports.js.map +1 -0
  90. package/dist/core/types.d.ts +150 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +45 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/core.d.ts +12 -0
  95. package/dist/core.d.ts.map +1 -0
  96. package/dist/core.js +11 -0
  97. package/dist/core.js.map +1 -0
  98. package/dist/index.d.ts +15 -6
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +13 -7
  101. package/dist/index.js.map +1 -1
  102. package/dist/react/accessible-rows.d.ts +13 -0
  103. package/dist/react/accessible-rows.d.ts.map +1 -0
  104. package/dist/react/accessible-rows.js +320 -0
  105. package/dist/react/accessible-rows.js.map +1 -0
  106. package/dist/react/cell-editor.d.ts +22 -0
  107. package/dist/react/cell-editor.d.ts.map +1 -0
  108. package/dist/react/cell-editor.js +431 -0
  109. package/dist/react/cell-editor.js.map +1 -0
  110. package/dist/react/cell-menu.d.ts +18 -0
  111. package/dist/react/cell-menu.d.ts.map +1 -0
  112. package/dist/react/cell-menu.js +299 -0
  113. package/dist/react/cell-menu.js.map +1 -0
  114. package/dist/react/column-menu.d.ts +16 -0
  115. package/dist/react/column-menu.d.ts.map +1 -0
  116. package/dist/react/column-menu.js +1311 -0
  117. package/dist/react/column-menu.js.map +1 -0
  118. package/dist/react/columns-menu.d.ts +7 -0
  119. package/dist/react/columns-menu.d.ts.map +1 -0
  120. package/dist/react/columns-menu.js +249 -0
  121. package/dist/react/columns-menu.js.map +1 -0
  122. package/dist/react/conditions-menu.d.ts +10 -0
  123. package/dist/react/conditions-menu.d.ts.map +1 -0
  124. package/dist/react/conditions-menu.js +795 -0
  125. package/dist/react/conditions-menu.js.map +1 -0
  126. package/dist/react/context.d.ts +29 -0
  127. package/dist/react/context.d.ts.map +1 -0
  128. package/dist/react/context.js +18 -0
  129. package/dist/react/context.js.map +1 -0
  130. package/dist/react/data-grid.d.ts +79 -0
  131. package/dist/react/data-grid.d.ts.map +1 -0
  132. package/dist/react/data-grid.js +653 -0
  133. package/dist/react/data-grid.js.map +1 -0
  134. package/dist/react/detail-panel.d.ts +22 -0
  135. package/dist/react/detail-panel.d.ts.map +1 -0
  136. package/dist/react/detail-panel.js +242 -0
  137. package/dist/react/detail-panel.js.map +1 -0
  138. package/dist/react/export-menu.d.ts +11 -0
  139. package/dist/react/export-menu.d.ts.map +1 -0
  140. package/dist/react/export-menu.js +304 -0
  141. package/dist/react/export-menu.js.map +1 -0
  142. package/dist/react/find-bar.d.ts +11 -0
  143. package/dist/react/find-bar.d.ts.map +1 -0
  144. package/dist/react/find-bar.js +303 -0
  145. package/dist/react/find-bar.js.map +1 -0
  146. package/dist/react/format-form.d.ts +12 -0
  147. package/dist/react/format-form.d.ts.map +1 -0
  148. package/dist/react/format-form.js +863 -0
  149. package/dist/react/format-form.js.map +1 -0
  150. package/dist/react/formula-menu.d.ts +6 -0
  151. package/dist/react/formula-menu.d.ts.map +1 -0
  152. package/dist/react/formula-menu.js +343 -0
  153. package/dist/react/formula-menu.js.map +1 -0
  154. package/dist/react/grid-body.d.ts +33 -0
  155. package/dist/react/grid-body.d.ts.map +1 -0
  156. package/dist/react/grid-body.js +1389 -0
  157. package/dist/react/grid-body.js.map +1 -0
  158. package/dist/react/group-menu.d.ts +7 -0
  159. package/dist/react/group-menu.d.ts.map +1 -0
  160. package/dist/react/group-menu.js +508 -0
  161. package/dist/react/group-menu.js.map +1 -0
  162. package/dist/react/hooks.d.ts +33 -0
  163. package/dist/react/hooks.d.ts.map +1 -0
  164. package/dist/react/hooks.js +161 -0
  165. package/dist/react/hooks.js.map +1 -0
  166. package/dist/react/messages.d.ts +180 -0
  167. package/dist/react/messages.d.ts.map +1 -0
  168. package/dist/react/messages.js +414 -0
  169. package/dist/react/messages.js.map +1 -0
  170. package/dist/react/save.d.ts +9 -0
  171. package/dist/react/save.d.ts.map +1 -0
  172. package/dist/react/save.js +102 -0
  173. package/dist/react/save.js.map +1 -0
  174. package/dist/react/static-rows.d.ts +9 -0
  175. package/dist/react/static-rows.d.ts.map +1 -0
  176. package/dist/react/static-rows.js +176 -0
  177. package/dist/react/static-rows.js.map +1 -0
  178. package/dist/react/status-bar.d.ts +12 -0
  179. package/dist/react/status-bar.d.ts.map +1 -0
  180. package/dist/react/status-bar.js +413 -0
  181. package/dist/react/status-bar.js.map +1 -0
  182. package/dist/react/toolbar-popover.d.ts +14 -0
  183. package/dist/react/toolbar-popover.d.ts.map +1 -0
  184. package/dist/react/toolbar-popover.js +143 -0
  185. package/dist/react/toolbar-popover.js.map +1 -0
  186. package/dist/react/toolbar.d.ts +13 -0
  187. package/dist/react/toolbar.d.ts.map +1 -0
  188. package/dist/react/toolbar.js +344 -0
  189. package/dist/react/toolbar.js.map +1 -0
  190. package/dist/server.d.ts +22 -0
  191. package/dist/server.d.ts.map +1 -0
  192. package/dist/server.js +45 -0
  193. package/dist/server.js.map +1 -0
  194. package/dist/styles.css +496 -316
  195. package/package.json +43 -18
  196. package/src/canvas/geometry.ts +197 -0
  197. package/src/canvas/grid-canvas.ts +1222 -0
  198. package/src/canvas/hooks.ts +61 -0
  199. package/src/canvas/keys.ts +96 -0
  200. package/src/canvas/layout.ts +77 -0
  201. package/src/canvas/scroll.ts +120 -0
  202. package/src/canvas/text.ts +155 -0
  203. package/src/{theme.ts → canvas/theme.ts} +14 -1
  204. package/src/core/cache.ts +247 -0
  205. package/src/core/checked.ts +64 -0
  206. package/src/core/client.ts +103 -0
  207. package/src/core/conditions.ts +89 -0
  208. package/src/core/evaluate.ts +221 -0
  209. package/src/core/filters.ts +268 -0
  210. package/src/core/format.ts +309 -0
  211. package/src/core/measures.ts +99 -0
  212. package/src/core/request-ids.ts +54 -0
  213. package/src/core/rows.ts +213 -0
  214. package/src/core/store.ts +1122 -0
  215. package/src/core/totals.ts +59 -0
  216. package/src/core/trace.ts +44 -0
  217. package/src/core/transports.ts +177 -0
  218. package/src/core/types.ts +168 -0
  219. package/src/core.ts +39 -0
  220. package/src/index.ts +24 -12
  221. package/src/react/accessible-rows.tsx +242 -0
  222. package/src/react/cell-editor.tsx +216 -0
  223. package/src/react/cell-menu.tsx +166 -0
  224. package/src/react/column-menu.tsx +619 -0
  225. package/src/react/columns-menu.tsx +116 -0
  226. package/src/react/conditions-menu.tsx +289 -0
  227. package/src/react/context.ts +33 -0
  228. package/src/react/data-grid.tsx +286 -0
  229. package/src/react/detail-panel.tsx +92 -0
  230. package/src/react/export-menu.tsx +112 -0
  231. package/src/react/find-bar.tsx +112 -0
  232. package/src/react/format-form.tsx +421 -0
  233. package/src/react/formula-menu.tsx +163 -0
  234. package/src/react/grid-body.tsx +744 -0
  235. package/src/react/group-menu.tsx +214 -0
  236. package/src/react/hooks.ts +104 -0
  237. package/src/react/messages.ts +556 -0
  238. package/src/react/save.ts +42 -0
  239. package/src/react/static-rows.tsx +59 -0
  240. package/src/react/status-bar.tsx +175 -0
  241. package/src/react/toolbar-popover.tsx +69 -0
  242. package/src/react/toolbar.tsx +152 -0
  243. package/src/server.ts +58 -0
  244. package/src/styles.css +496 -316
  245. package/dist/components/bars.d.ts +0 -14
  246. package/dist/components/bars.d.ts.map +0 -1
  247. package/dist/components/bars.js +0 -67
  248. package/dist/components/bars.js.map +0 -1
  249. package/dist/components/cell-inspector.d.ts +0 -11
  250. package/dist/components/cell-inspector.d.ts.map +0 -1
  251. package/dist/components/cell-inspector.js +0 -37
  252. package/dist/components/cell-inspector.js.map +0 -1
  253. package/dist/components/columns-panel.d.ts +0 -12
  254. package/dist/components/columns-panel.d.ts.map +0 -1
  255. package/dist/components/columns-panel.js +0 -11
  256. package/dist/components/columns-panel.js.map +0 -1
  257. package/dist/components/filter-panel.d.ts +0 -17
  258. package/dist/components/filter-panel.d.ts.map +0 -1
  259. package/dist/components/filter-panel.js +0 -165
  260. package/dist/components/filter-panel.js.map +0 -1
  261. package/dist/components/find-bar.d.ts +0 -9
  262. package/dist/components/find-bar.d.ts.map +0 -1
  263. package/dist/components/find-bar.js +0 -46
  264. package/dist/components/find-bar.js.map +0 -1
  265. package/dist/components/popover.d.ts +0 -38
  266. package/dist/components/popover.d.ts.map +0 -1
  267. package/dist/components/popover.js +0 -104
  268. package/dist/components/popover.js.map +0 -1
  269. package/dist/components/stats-panel.d.ts +0 -12
  270. package/dist/components/stats-panel.d.ts.map +0 -1
  271. package/dist/components/stats-panel.js +0 -40
  272. package/dist/components/stats-panel.js.map +0 -1
  273. package/dist/controller.d.ts +0 -218
  274. package/dist/controller.d.ts.map +0 -1
  275. package/dist/controller.js +0 -788
  276. package/dist/controller.js.map +0 -1
  277. package/dist/data-grid.d.ts +0 -51
  278. package/dist/data-grid.d.ts.map +0 -1
  279. package/dist/data-grid.js +0 -698
  280. package/dist/data-grid.js.map +0 -1
  281. package/dist/labels.d.ts +0 -9
  282. package/dist/labels.d.ts.map +0 -1
  283. package/dist/labels.js +0 -38
  284. package/dist/labels.js.map +0 -1
  285. package/dist/messages.d.ts +0 -99
  286. package/dist/messages.d.ts.map +0 -1
  287. package/dist/messages.js +0 -276
  288. package/dist/messages.js.map +0 -1
  289. package/dist/renderer.d.ts +0 -52
  290. package/dist/renderer.d.ts.map +0 -1
  291. package/dist/renderer.js +0 -522
  292. package/dist/renderer.js.map +0 -1
  293. package/dist/text.d.ts +0 -14
  294. package/dist/text.d.ts.map +0 -1
  295. package/dist/text.js +0 -63
  296. package/dist/text.js.map +0 -1
  297. package/dist/theme.d.ts.map +0 -1
  298. package/dist/theme.js +0 -54
  299. package/dist/theme.js.map +0 -1
  300. package/src/components/bars.tsx +0 -179
  301. package/src/components/cell-inspector.tsx +0 -70
  302. package/src/components/columns-panel.tsx +0 -59
  303. package/src/components/filter-panel.tsx +0 -290
  304. package/src/components/find-bar.tsx +0 -104
  305. package/src/components/popover.tsx +0 -194
  306. package/src/components/stats-panel.tsx +0 -153
  307. package/src/controller.ts +0 -887
  308. package/src/data-grid.tsx +0 -1029
  309. package/src/labels.ts +0 -46
  310. package/src/messages.ts +0 -390
  311. package/src/renderer.ts +0 -535
  312. package/src/text.ts +0 -55
@@ -0,0 +1,116 @@
1
+ import { use, useState, useTransition } from "react";
2
+ import type { GridCommand } from "vastgrid-spec";
3
+ import { LocaleContext } from "./context.js";
4
+ import { useGridSelector, useGridStore } from "./hooks.js";
5
+
6
+ /**
7
+ * The content of the toolbar's columns menu, loaded when it first opens (§6.3 of the specification): every column of the view, the displayed ones in display
8
+ * order and then the hidden ones, each with a check box that shows or hides it (`column.show`, `column.hide`) and
9
+ * buttons that move it by one place (`column.move`), and a box that finds columns by name.
10
+ */
11
+ export default function ColumnsPanel() {
12
+ const store = useGridStore();
13
+ const { messages } = use(LocaleContext);
14
+ const displayed = useGridSelector((snapshot) => snapshot.displayed);
15
+ const view = useGridSelector((snapshot) => snapshot.view?.columns);
16
+ const [find, setFind] = useState("");
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [pending, startDispatch] = useTransition();
19
+ if (!view) return null;
20
+
21
+ const shown = new Set(displayed.map((item) => item.column.name));
22
+ const hidden = view.filter((column) => !shown.has(column.name));
23
+ const needle = find.trim().toLowerCase();
24
+ const matches = (name: string) => needle === "" || name.toLowerCase().includes(needle);
25
+
26
+ const run = (command: GridCommand) =>
27
+ startDispatch(async () => {
28
+ const outcome = await store.dispatch(command);
29
+ setError(outcome.ok ? null : outcome.error.message);
30
+ });
31
+
32
+ return (
33
+ <>
34
+ <input
35
+ type="search"
36
+ className="vg-input"
37
+ aria-label={messages.findColumn}
38
+ placeholder={messages.findColumn}
39
+ value={find}
40
+ onChange={(event) => setFind(event.target.value)}
41
+ />
42
+ <ul className="vg-columns__list" aria-busy={pending}>
43
+ {displayed.map((item, index) =>
44
+ matches(item.column.name) ? (
45
+ <li key={item.column.name} className="vg-columns__item">
46
+ <label>
47
+ <input
48
+ type="checkbox"
49
+ checked
50
+ onChange={() => run({ command: "column.hide", names: [item.column.name] })}
51
+ />
52
+ <span className="vg-filter__label">{item.column.name}</span>
53
+ </label>
54
+ {item.setting.pin && <span className="vg-filter__count">{messages.pinned[item.setting.pin]}</span>}
55
+ <button
56
+ type="button"
57
+ className="vg-button vg-button--small"
58
+ aria-label={messages.moveUp(item.column.name)}
59
+ disabled={index === 0}
60
+ onClick={() =>
61
+ run({ command: "column.move", names: [item.column.name], before: displayed[index - 1].column.name })
62
+ }
63
+ >
64
+ ↑
65
+ </button>
66
+ <button
67
+ type="button"
68
+ className="vg-button vg-button--small"
69
+ aria-label={messages.moveDown(item.column.name)}
70
+ disabled={index === displayed.length - 1}
71
+ onClick={() =>
72
+ run({
73
+ command: "column.move",
74
+ names: [item.column.name],
75
+ before: displayed[index + 2]?.column.name ?? null,
76
+ })
77
+ }
78
+ >
79
+ ↓
80
+ </button>
81
+ </li>
82
+ ) : null,
83
+ )}
84
+ {hidden
85
+ .filter((column) => matches(column.name))
86
+ .map((column) => (
87
+ <li key={column.name} className="vg-columns__item">
88
+ <label>
89
+ <input
90
+ type="checkbox"
91
+ checked={false}
92
+ onChange={() => run({ command: "column.show", names: [column.name] })}
93
+ />
94
+ <span className="vg-filter__label">{column.name}</span>
95
+ </label>
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ {error !== null && (
100
+ <p role="alert" className="vg-filter__error">
101
+ {error}
102
+ </p>
103
+ )}
104
+ {hidden.length > 0 && (
105
+ <button
106
+ type="button"
107
+ className="vg-menu__item"
108
+ disabled={pending}
109
+ onClick={() => run({ command: "column.show", all: true })}
110
+ >
111
+ {messages.showAllColumns}
112
+ </button>
113
+ )}
114
+ </>
115
+ );
116
+ }
@@ -0,0 +1,289 @@
1
+ import { use, useState, useTransition } from "react";
2
+ import { type Column, type Expr, formulaText } from "vastgrid-spec";
3
+ import { ConditionError, type ConditionNode, nodeId, readConditions, writeConditions } from "../core/conditions.js";
4
+ import { type FilterOperator, operatorsFor, takesNoValue } from "../core/filters.js";
5
+ import { rowColumns } from "../core/measures.js";
6
+ import { LocaleContext } from "./context.js";
7
+ import { useGridSelector, useGridStore } from "./hooks.js";
8
+ import type { GridMessages } from "./messages.js";
9
+
10
+ type Group = ConditionNode & { kind: "group" };
11
+
12
+ /** A tree with one node changed, or removed when `change` gives null. */
13
+ function changed(group: Group, id: number, change: (node: ConditionNode) => ConditionNode | null): Group {
14
+ const items = group.items.flatMap((item): ConditionNode[] => {
15
+ if (item.id === id) {
16
+ const next = change(item);
17
+ return next ? [next] : [];
18
+ }
19
+ return item.kind === "group" ? [changed(item, id, change)] : [item];
20
+ });
21
+ return group.id === id ? ((change(group) as Group | null) ?? { ...group, items: [] }) : { ...group, items };
22
+ }
23
+
24
+ /** A condition on the first column, with the first operator of its type. */
25
+ function newCondition(columns: readonly Column[]): ConditionNode {
26
+ const column = columns[0];
27
+ return {
28
+ kind: "condition",
29
+ id: nodeId(),
30
+ column: column?.name ?? "",
31
+ filter: { kind: "condition", operator: column ? operatorsFor(column.type)[0] : "eq", first: "", second: "" },
32
+ };
33
+ }
34
+
35
+ /** An expression as a formula (§4.11), or its JSON when it has none. */
36
+ function expressionText(expr: Expr): string {
37
+ try {
38
+ return formulaText(expr);
39
+ } catch {
40
+ return JSON.stringify(expr);
41
+ }
42
+ }
43
+
44
+ interface NodeProps {
45
+ node: ConditionNode;
46
+ columns: readonly Column[];
47
+ invalid: number | null;
48
+ messages: GridMessages;
49
+ onChange: (id: number, change: (node: ConditionNode) => ConditionNode | null) => void;
50
+ }
51
+
52
+ /** A condition: its column, operator and texts. */
53
+ function ConditionRow({ node, columns, invalid, messages, onChange }: NodeProps & { node: { kind: "condition" } }) {
54
+ const column = columns.find((item) => item.name === node.column);
55
+ const operators = column ? operatorsFor(column.type) : [];
56
+ const { operator, first, second } = node.filter;
57
+ // Changes apply to the node as the draft holds it when they apply.
58
+ const set = (filter: Partial<typeof node.filter>) =>
59
+ onChange(node.id, (current) =>
60
+ current.kind === "condition" ? { ...current, filter: { ...current.filter, ...filter } } : current,
61
+ );
62
+ return (
63
+ <li className="vg-conditions__row">
64
+ <select
65
+ className="vg-input"
66
+ aria-label={messages.conditionColumn}
67
+ value={node.column}
68
+ onChange={(event) => {
69
+ const next = columns.find((item) => item.name === event.target.value);
70
+ if (!next) return;
71
+ const kept = operatorsFor(next.type).includes(operator) ? operator : operatorsFor(next.type)[0];
72
+ onChange(node.id, (current) =>
73
+ current.kind === "condition"
74
+ ? { ...current, column: next.name, filter: { ...current.filter, operator: kept } }
75
+ : current,
76
+ );
77
+ }}
78
+ >
79
+ {columns.map((item) => (
80
+ <option key={item.name} value={item.name}>
81
+ {item.name}
82
+ </option>
83
+ ))}
84
+ </select>
85
+ <select
86
+ className="vg-input"
87
+ aria-label={messages.condition}
88
+ value={operator}
89
+ onChange={(event) => set({ operator: event.target.value as FilterOperator })}
90
+ >
91
+ {operators.map((item) => (
92
+ <option key={item} value={item}>
93
+ {messages.operators[item]}
94
+ </option>
95
+ ))}
96
+ </select>
97
+ {!takesNoValue(operator) && (
98
+ <input
99
+ className="vg-input"
100
+ aria-label={operator === "between" ? messages.minimum : messages.value}
101
+ placeholder={operator === "between" ? messages.minimum : messages.value}
102
+ aria-invalid={invalid === node.id}
103
+ value={first}
104
+ onChange={(event) => set({ first: event.target.value })}
105
+ />
106
+ )}
107
+ {operator === "between" && (
108
+ <input
109
+ className="vg-input"
110
+ aria-label={messages.maximum}
111
+ placeholder={messages.maximum}
112
+ aria-invalid={invalid === node.id}
113
+ value={second}
114
+ onChange={(event) => set({ second: event.target.value })}
115
+ />
116
+ )}
117
+ <button
118
+ type="button"
119
+ className="vg-button vg-button--small"
120
+ aria-label={messages.removeCondition}
121
+ onClick={() => onChange(node.id, () => null)}
122
+ >
123
+ ×
124
+ </button>
125
+ </li>
126
+ );
127
+ }
128
+
129
+ /** A group: whether all of its items hold or one of them, its items, and buttons that add to it. */
130
+ function GroupBlock({ node, columns, invalid, messages, onChange, top }: NodeProps & { node: Group; top?: boolean }) {
131
+ return (
132
+ <fieldset className={top ? "vg-conditions__group vg-conditions__group--top" : "vg-conditions__group"}>
133
+ <legend>
134
+ <select
135
+ className="vg-input"
136
+ aria-label={messages.matchLabel}
137
+ value={node.op}
138
+ onChange={(event) => {
139
+ const op = event.target.value as "and" | "or";
140
+ onChange(node.id, (current) => (current.kind === "group" ? { ...current, op } : current));
141
+ }}
142
+ >
143
+ <option value="and">{messages.matchAll}</option>
144
+ <option value="or">{messages.matchAny}</option>
145
+ </select>
146
+ </legend>
147
+ {node.items.length === 0 && top && <p className="vg-filter__note">{messages.noConditions}</p>}
148
+ <ul className="vg-conditions__items">
149
+ {node.items.map((item) =>
150
+ item.kind === "group" ? (
151
+ <li key={item.id}>
152
+ <GroupBlock node={item} columns={columns} invalid={invalid} messages={messages} onChange={onChange} />
153
+ </li>
154
+ ) : item.kind === "condition" ? (
155
+ <ConditionRow
156
+ key={item.id}
157
+ node={item}
158
+ columns={columns}
159
+ invalid={invalid}
160
+ messages={messages}
161
+ onChange={onChange}
162
+ />
163
+ ) : (
164
+ <li key={item.id} className="vg-conditions__row">
165
+ <code className="vg-conditions__expression">{expressionText(item.expr)}</code>
166
+ <button
167
+ type="button"
168
+ className="vg-button vg-button--small"
169
+ aria-label={messages.removeCondition}
170
+ onClick={() => onChange(item.id, () => null)}
171
+ >
172
+ ×
173
+ </button>
174
+ </li>
175
+ ),
176
+ )}
177
+ </ul>
178
+ <div className="vg-conditions__add">
179
+ <button
180
+ type="button"
181
+ className="vg-button vg-button--small"
182
+ onClick={() => {
183
+ const added = newCondition(columns);
184
+ onChange(node.id, (current) =>
185
+ current.kind === "group" ? { ...current, items: [...current.items, added] } : current,
186
+ );
187
+ }}
188
+ >
189
+ {messages.addCondition}
190
+ </button>
191
+ <button
192
+ type="button"
193
+ className="vg-button vg-button--small"
194
+ onClick={() => {
195
+ const added: ConditionNode = {
196
+ kind: "group",
197
+ id: nodeId(),
198
+ op: node.op === "and" ? "or" : "and",
199
+ items: [newCondition(columns)],
200
+ };
201
+ onChange(node.id, (current) =>
202
+ current.kind === "group" ? { ...current, items: [...current.items, added] } : current,
203
+ );
204
+ }}
205
+ >
206
+ {messages.addGroup}
207
+ </button>
208
+ {!top && (
209
+ <button type="button" className="vg-button vg-button--small" onClick={() => onChange(node.id, () => null)}>
210
+ {messages.removeGroup}
211
+ </button>
212
+ )}
213
+ </div>
214
+ </fieldset>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * The content of the toolbar's conditions menu (§6.2.1 of the specification): every condition of the plan, those
220
+ * across columns included, as groups of conditions that all hold or one of which holds, nested. It reads `where`
221
+ * when it opens, and applying dispatches `filter.set`, which keeps the search. Expressions that it does not take
222
+ * apart are shown as formulas and kept, or removed. The toolbar loads it when the menu first opens.
223
+ */
224
+ export default function ConditionsPanel({ close }: { close: () => void }) {
225
+ const store = useGridStore();
226
+ const { messages } = use(LocaleContext);
227
+ const table = useGridSelector((snapshot) => snapshot.table);
228
+ const plan = useGridSelector((snapshot) => snapshot.document.plan);
229
+ // The conditions as they are when the menu opens, an agent's or the column menu's included.
230
+ const [draft, setDraft] = useState<Group>(() => readConditions(store.getSnapshot().document.plan?.where));
231
+ const [invalid, setInvalid] = useState<number | null>(null);
232
+ const [error, setError] = useState<string | null>(null);
233
+ const [pending, startApply] = useTransition();
234
+ if (!table) return null;
235
+ const columns = rowColumns(plan, table);
236
+
237
+ const apply = (group: Group) => {
238
+ let where: Expr | null;
239
+ try {
240
+ where = writeConditions(group, columns);
241
+ } catch (failure) {
242
+ if (!(failure instanceof ConditionError)) throw failure;
243
+ setInvalid(failure.node);
244
+ setError(messages.invalidCondition);
245
+ return;
246
+ }
247
+ startApply(async () => {
248
+ const outcome = await store.dispatch({ command: "filter.set", where });
249
+ if (!outcome.ok) {
250
+ setError(outcome.error.message);
251
+ return;
252
+ }
253
+ setInvalid(null);
254
+ setError(null);
255
+ close();
256
+ });
257
+ };
258
+
259
+ return (
260
+ <form
261
+ onSubmit={(event) => {
262
+ event.preventDefault();
263
+ apply(draft);
264
+ }}
265
+ >
266
+ <GroupBlock
267
+ top
268
+ node={draft}
269
+ columns={columns}
270
+ invalid={invalid}
271
+ messages={messages}
272
+ onChange={(target, change) => setDraft((current) => changed(current, target, change))}
273
+ />
274
+ {error !== null && (
275
+ <p role="alert" className="vg-filter__error">
276
+ {error}
277
+ </p>
278
+ )}
279
+ <div className="vg-filter__actions">
280
+ <button type="button" className="vg-button" disabled={pending} onClick={() => apply({ ...draft, items: [] })}>
281
+ {messages.clearConditions}
282
+ </button>
283
+ <button type="submit" className="vg-button vg-button--primary" disabled={pending}>
284
+ {messages.apply}
285
+ </button>
286
+ </div>
287
+ </form>
288
+ );
289
+ }
@@ -0,0 +1,33 @@
1
+ import { createContext } from "react";
2
+ import type { GridStore } from "../core/store.js";
3
+ import { type GridMessages, messagesFor } from "./messages.js";
4
+
5
+ /** The store of the nearest `DataGrid` or `GridProvider`. */
6
+ export const GridContext = createContext<GridStore | null>(null);
7
+
8
+ /** The UI's texts and number formats of the nearest `DataGrid`. */
9
+ export interface GridLocale {
10
+ messages: GridMessages;
11
+ locale: string | undefined;
12
+ number: Intl.NumberFormat;
13
+ }
14
+
15
+ export const LocaleContext = createContext<GridLocale>({
16
+ messages: messagesFor(undefined),
17
+ locale: undefined,
18
+ number: new Intl.NumberFormat("en"),
19
+ });
20
+
21
+ /** Saving in the nearest `DataGrid` (§6.2 of react.md): an Action, with its pending state and last outcome. */
22
+ export interface GridSave {
23
+ /** Saves the written cells (the `save` command); a save asked for while one runs waits for it. */
24
+ save: () => void;
25
+ pending: boolean;
26
+ /**
27
+ * The text of the last save's outcome, and the revision and edits version it is about: shown until the cells or
28
+ * the revision change.
29
+ */
30
+ outcome: { text: string; failed: boolean; revision: string | null; version: number } | null;
31
+ }
32
+
33
+ export const SaveContext = createContext<GridSave | null>(null);