@protobase/ui 0.19.15 → 0.20.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 (80) hide show
  1. package/dist/app/app.js +2 -2
  2. package/dist/app/charts/chart-groups.js +52 -0
  3. package/dist/app/charts/chart-view.js +75 -0
  4. package/dist/app/charts/group-bars.js +49 -0
  5. package/dist/app/charts/group-donut.js +84 -0
  6. package/dist/app/charts/period-chart.js +174 -0
  7. package/dist/app/displays/board-column.js +114 -0
  8. package/dist/app/displays/board-columns.js +45 -0
  9. package/dist/app/displays/board-display.js +96 -0
  10. package/dist/app/displays/board-dnd.js +32 -0
  11. package/dist/app/displays/list-displays.js +13 -1
  12. package/dist/app/displays/record-card.js +16 -5
  13. package/dist/app/displays/use-board-column.js +23 -0
  14. package/dist/app/displays/use-board-move.js +60 -0
  15. package/dist/app/displays/virtual-cards.js +1 -0
  16. package/dist/app/embedded-list/embedded-list.js +9 -0
  17. package/dist/app/export/export-options.js +2 -2
  18. package/dist/app/gallery/use-gallery-photos.js +2 -2
  19. package/dist/app/import/lookups.js +2 -2
  20. package/dist/app/list-edit/cell-field-editor.js +61 -0
  21. package/dist/app/list-edit/use-list-cell-editor.js +30 -0
  22. package/dist/app/list-edit/use-list-edits.js +75 -0
  23. package/dist/app/list-group-chart.js +37 -0
  24. package/dist/app/list-page.js +38 -13
  25. package/dist/app/map/map-area.js +6 -6
  26. package/dist/app/pages/action-href.js +4 -2
  27. package/dist/app/pages/blocks/chart.js +26 -0
  28. package/dist/app/pages/render-node.js +2 -0
  29. package/dist/app/pages/use-run-action.js +4 -3
  30. package/dist/app/record-page.js +30 -16
  31. package/dist/app/record-preview.js +25 -0
  32. package/dist/app/record-row/record-row-body.js +2 -2
  33. package/dist/app/record-row/record-row.js +1 -0
  34. package/dist/app/related-dots/card-dots-lines.js +16 -0
  35. package/dist/app/related-dots/card-dots.js +34 -0
  36. package/dist/app/related-dots/dot-card.js +3 -3
  37. package/dist/app/related-dots/related-dots-columns.js +3 -1
  38. package/dist/app/related-dots/related-dots.js +22 -9
  39. package/dist/app/related-section.js +4 -4
  40. package/dist/app/relation-options.js +44 -0
  41. package/dist/app/relation-picker.js +114 -35
  42. package/dist/app/saved/saved-records.js +2 -2
  43. package/dist/app/search/search-model.js +2 -2
  44. package/dist/app/searches/saved-search.js +3 -3
  45. package/dist/app/searches/search-diff.js +8 -8
  46. package/dist/app/use-nav-recent.js +1 -2
  47. package/dist/data/api-provider.d.ts +4 -1
  48. package/dist/data/api-provider.js +12 -5
  49. package/dist/data/focus-refresh.js +44 -0
  50. package/dist/data/query-keys.js +18 -0
  51. package/dist/data/use-aggregates.js +14 -1
  52. package/dist/data/use-record.js +17 -2
  53. package/dist/data-grid/cell-moves.js +26 -0
  54. package/dist/data-grid/data-grid.js +32 -7
  55. package/dist/data-grid/grid-header.js +1 -1
  56. package/dist/data-grid/grid-row.js +17 -13
  57. package/dist/data-grid/page-cache.js +3 -0
  58. package/dist/data-grid/use-cell-editing.js +113 -0
  59. package/dist/data-grid/use-paged-rows.js +25 -4
  60. package/dist/index.js +2 -2
  61. package/dist/live/field-values.js +3 -1
  62. package/dist/live/related-column-rows.js +3 -3
  63. package/dist/live/relation-labels.js +2 -2
  64. package/dist/live/use-live-rows.js +17 -4
  65. package/dist/live/use-reference-search.js +2 -2
  66. package/dist/package.js +1 -1
  67. package/dist/shell/assets/app-B5h_q_JL.js +69 -0
  68. package/dist/shell/assets/{index-CzV9U_NJ.js → index-B0veahji.js} +1 -1
  69. package/dist/shell/assets/index-BZmb28OT.css +2 -0
  70. package/dist/shell/assets/{maplibre-G35v6TYU.js → maplibre-CbjT6RKG.js} +1 -1
  71. package/dist/shell/assets/{markdown-content-DGFnggSX.js → markdown-content-BjRGtSOA.js} +1 -1
  72. package/dist/shell/assets/{record-map-ZAC_U52Y.js → record-map-rmkTnAs8.js} +1 -1
  73. package/dist/shell/assets/{records-map-DLXOQVpM.js → records-map-D1vLGY06.js} +1 -1
  74. package/dist/shell/assets/{shared-protobase-ui-xByMDSPg.js → shared-protobase-ui-PnkJqBqB.js} +1 -1
  75. package/dist/shell/index.html +4 -4
  76. package/dist/shell/protobase.shell.json +1 -1
  77. package/dist/tokens.css +17 -0
  78. package/package.json +4 -4
  79. package/dist/shell/assets/app-WromtADN.js +0 -69
  80. package/dist/shell/assets/index-DEEIUcYb.css +0 -2
@@ -10,12 +10,13 @@ import { GridHeader } from "./grid-header.js";
10
10
  import { DataRow, SkeletonRow } from "./grid-row.js";
11
11
  import { usePagedRows } from "./use-paged-rows.js";
12
12
  import { useVirtualRows } from "./use-virtual-rows.js";
13
+ import { useCellEditing } from "./use-cell-editing.js";
13
14
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
14
15
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
15
16
  import { useTable } from "@tanstack/react-table";
16
17
  //#region src/data-grid/data-grid.tsx
17
18
  const headerHeight = 32;
18
- const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable = true, fit = false, scroll = "own", className }) => {
19
+ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, cellEditor, rowPatches, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable = true, fit = false, scroll = "own", className }) => {
19
20
  const rowHeight = compact ? 28 : 36;
20
21
  const [pinned, setPinned] = usePinnedColumns(pinKey, defaultPinned);
21
22
  const columns = useMemo(() => {
@@ -65,15 +66,24 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
65
66
  total,
66
67
  rowAt: sourceRowAt ? rowAt : void 0,
67
68
  loadRows: source.loadRows,
69
+ revision: source.revision,
68
70
  latencyMs
69
71
  }, firstIndex, lastIndex);
72
+ const patchedRow = useCallback((index) => {
73
+ const row = getRow(index);
74
+ const patch = row && rowPatches?.get(row.id);
75
+ return patch ? {
76
+ ...row,
77
+ ...patch
78
+ } : row;
79
+ }, [getRow, rowPatches]);
70
80
  const loaded = virtualRows.map(({ index, offset }) => ({
71
81
  index,
72
82
  offset,
73
- row: getRow(index)
83
+ row: patchedRow(index)
74
84
  }));
75
85
  const data = useMemo(() => loaded.flatMap(({ row }) => row ? [row] : []), [
76
- getRow,
86
+ patchedRow,
77
87
  firstIndex,
78
88
  lastIndex
79
89
  ]);
@@ -123,6 +133,16 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
123
133
  sorted: header.column.getIsSorted(),
124
134
  onSort: (event) => header.column.getToggleSortingHandler()?.(event)
125
135
  })));
136
+ const gridRef = useRef(null);
137
+ const columnIds = useMemo(() => columns.map((spec) => spec.id), [columns]);
138
+ const cellEditing = useCellEditing({
139
+ editor: cellEditor,
140
+ columns: columnIds,
141
+ total,
142
+ rowAt: patchedRow,
143
+ onOpenRow,
144
+ container: gridRef
145
+ });
126
146
  const minWidth = gridMinWidth(columns, gutter, trailing, minScale);
127
147
  const own = scroll === "own";
128
148
  const header = /* @__PURE__ */ jsx("div", {
@@ -183,17 +203,22 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
183
203
  row,
184
204
  actions
185
205
  }) : void 0,
186
- cells: tableRow.getAllCells().map((cell) => ({
187
- spec: columns.find((spec) => spec.id === cell.column.id),
188
- content: /* @__PURE__ */ jsx(table.FlexRender, { cell })
189
- }))
206
+ cells: tableRow.getAllCells().map((cell) => {
207
+ const content = /* @__PURE__ */ jsx(table.FlexRender, { cell });
208
+ return {
209
+ spec: columns.find((spec) => spec.id === cell.column.id),
210
+ ...cellEditing && row ? cellEditing.cell(index, row, cell.column.id, content) : { content }
211
+ };
212
+ })
190
213
  }, row?.id);
191
214
  })
192
215
  })
193
216
  });
194
217
  return /* @__PURE__ */ jsxs("div", {
218
+ ref: gridRef,
195
219
  role: "grid",
196
220
  "aria-rowcount": total,
221
+ onKeyDown: cellEditing?.onKeyDown,
197
222
  className: cn("flex flex-col rounded-lg border bg-background", own ? "min-h-0 overflow-hidden" : "overflow-clip", className),
198
223
  style: fit && own ? { height: headerHeight + total * rowHeight + 2 } : void 0,
199
224
  children: [/* @__PURE__ */ jsxs("div", {
@@ -62,7 +62,7 @@ const GridHeader = ({ cells, specs, gutter, selectedCount, selectable, trailing,
62
62
  })]
63
63
  }, spec.id);
64
64
  }),
65
- trailing ? /* @__PURE__ */ jsx("div", {}) : null
65
+ trailing ? /* @__PURE__ */ jsx("div", { className: "sticky right-0 z-10 h-full bg-inherit" }) : null
66
66
  ]
67
67
  });
68
68
  //#endregion
@@ -14,17 +14,20 @@ const frameStyle = (frame) => ({
14
14
  /** Pinned cells need a solid background; `bg-inherit` takes the row's, so hover and selection still show. */
15
15
  const cellClass = (spec, frame) => cn("flex h-full min-w-0 items-center overflow-hidden whitespace-nowrap px-3", isRightAligned(spec.kind) && "justify-end", spec.highlight && "bg-primary-soft/60", frame.pins && spec.id in frame.pins && "sticky z-10 bg-inherit", frame.scrolled && spec.id === frame.lastPinned && "shadow-[1px_0_0_var(--border),4px_0_6px_-2px_rgb(0_0_0/0.12)]");
16
16
  const pinStyle = (spec, pins) => pins && spec.id in pins ? { left: pins[spec.id] } : void 0;
17
- /** The selection column never scrolls away, so rows can be selected from anywhere in a wide grid. */
17
+ /**
18
+ * The selection column never scrolls away, so rows can be selected from anywhere in a wide grid.
19
+ * The whole cell toggles the row instead of opening it; the checkbox's own click bubbles up to it.
20
+ */
18
21
  const Gutter = ({ width, index, selectable, selected, onToggle, edge }) => /* @__PURE__ */ jsxs("div", {
19
- className: cn("sticky left-0 z-10 flex h-full items-center gap-2 bg-inherit pl-3", edge && "shadow-[1px_0_0_var(--border),4px_0_6px_-2px_rgb(0_0_0/0.12)]"),
22
+ onClick: selectable ? (event) => {
23
+ event.stopPropagation();
24
+ onToggle?.();
25
+ } : void 0,
26
+ className: cn("sticky left-0 z-10 flex h-full items-center gap-2 bg-inherit pl-3", selectable && "cursor-default", edge && "shadow-[1px_0_0_var(--border),4px_0_6px_-2px_rgb(0_0_0/0.12)]"),
20
27
  style: { width },
21
- children: [selectable && /* @__PURE__ */ jsx("span", {
22
- onClick: (event) => event.stopPropagation(),
23
- children: /* @__PURE__ */ jsx(Checkbox, {
24
- checked: Boolean(selected),
25
- onChange: onToggle,
26
- label: `Select row ${index + 1}`
27
- })
28
+ children: [selectable && /* @__PURE__ */ jsx(Checkbox, {
29
+ checked: Boolean(selected),
30
+ label: `Select row ${index + 1}`
28
31
  }), width > 40 && /* @__PURE__ */ jsx("span", {
29
32
  className: "min-w-6 text-right text-[11px] tabular-nums text-faint-foreground",
30
33
  children: formatInt(index + 1)
@@ -44,14 +47,15 @@ const DataRow = ({ cells, selected, onToggle, onOpen, menu, ...frame }) => /* @_
44
47
  onToggle,
45
48
  edge: frame.scrolled && !frame.lastPinned
46
49
  }),
47
- cells.map(({ spec, content }) => /* @__PURE__ */ jsx("div", {
48
- role: "cell",
49
- className: cellClass(spec, frame),
50
+ cells.map(({ spec, content, props, chosen, editing, editable }) => /* @__PURE__ */ jsx("div", {
51
+ role: props ? "gridcell" : "cell",
52
+ ...props,
53
+ className: cn(cellClass(spec, frame), props && "scroll-mb-[calc(var(--pb-sticky-bottom,0px)+3rem)] scroll-mt-[calc(var(--pb-sticky-top,0px)+2rem)] outline-none", editable && !editing && "cursor-default", chosen && "shadow-[inset_0_0_0_2px_var(--primary)]", editing && "z-30 overflow-visible bg-background px-1"),
50
54
  style: pinStyle(spec, frame.pins),
51
55
  children: content
52
56
  }, spec.id)),
53
57
  menu && /* @__PURE__ */ jsx("div", {
54
- className: "flex h-full items-center justify-center",
58
+ className: "sticky right-0 z-10 flex h-full items-center justify-center bg-inherit",
55
59
  children: menu
56
60
  })
57
61
  ]
@@ -13,6 +13,9 @@ const createPageCache = (pageSize, maxPages) => {
13
13
  if (oldest.done) return;
14
14
  pages.delete(oldest.value);
15
15
  },
16
+ keepOnly: (kept) => {
17
+ for (const page of [...pages.keys()]) if (!kept.includes(page)) pages.delete(page);
18
+ },
16
19
  get size() {
17
20
  return pages.size;
18
21
  }
@@ -0,0 +1,113 @@
1
+ import { isMoveKey, movedCell, nextEditable } from "./cell-moves.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ //#region src/data-grid/use-cell-editing.tsx
4
+ const placeId = (place) => `${place.index}:${place.column}`;
5
+ /**
6
+ * The chosen cell and the one being edited, of a grid with an editor. The arrow keys, and Tab and Shift+Tab within a
7
+ * row, move between cells; Enter opens the editor of an editable cell and the row of any other; Escape lets go of the
8
+ * cell. A cell's mouse and keyboard handlers come from `cell`.
9
+ */
10
+ const useCellEditing = ({ editor, columns, total, rowAt, onOpenRow, container }) => {
11
+ const [active, setActive] = useState();
12
+ const [editing, setEditing] = useState(false);
13
+ const refocus = useRef(false);
14
+ const pressedChosen = useRef(false);
15
+ useEffect(() => {
16
+ if (!active || editing || !refocus.current) return;
17
+ refocus.current = false;
18
+ const cell = container.current?.querySelector(`[data-cell="${placeId(active)}"]`);
19
+ if (!cell) return;
20
+ cell.focus({ preventScroll: true });
21
+ cell.scrollIntoView({
22
+ block: "nearest",
23
+ inline: "nearest"
24
+ });
25
+ }, [
26
+ active,
27
+ editing,
28
+ container
29
+ ]);
30
+ if (!editor) return void 0;
31
+ const editableAt = (place) => {
32
+ const row = rowAt(place.index);
33
+ return row !== void 0 && editor.editable(row, place.column);
34
+ };
35
+ const choose = (place, edit = false) => {
36
+ refocus.current = true;
37
+ setActive(place);
38
+ setEditing(edit && editableAt(place));
39
+ };
40
+ const close = (move) => {
41
+ if (!active) return;
42
+ const row = rowAt(active.index);
43
+ const next = move && row ? nextEditable(active.column, columns, (column) => editor.editable(row, column), move) : void 0;
44
+ const focused = Boolean(container.current?.contains(document.activeElement));
45
+ choose(next ? {
46
+ ...active,
47
+ column: next
48
+ } : active, next !== void 0);
49
+ refocus.current = focused;
50
+ };
51
+ const onKeyDown = (event) => {
52
+ if (editing || !active || !(event.target instanceof HTMLElement) || event.target.dataset.cell !== placeId(active)) return;
53
+ const key = event.key === "Tab" ? event.shiftKey ? "ArrowLeft" : "ArrowRight" : event.key;
54
+ if (isMoveKey(key)) {
55
+ const next = movedCell(active, key, columns, total);
56
+ if (!next && event.key === "Tab") return;
57
+ event.preventDefault();
58
+ if (next && rowAt(next.index)) choose(next);
59
+ return;
60
+ }
61
+ if (event.key === "Enter" || event.key === "F2") {
62
+ event.preventDefault();
63
+ const row = rowAt(active.index);
64
+ if (editableAt(active)) choose(active, true);
65
+ else if (event.key === "Enter" && row) onOpenRow?.(row);
66
+ return;
67
+ }
68
+ if (event.key === "Escape") setActive(void 0);
69
+ };
70
+ /** What a cell of the grid needs: its content, its handlers, and whether it is chosen or being edited. */
71
+ const cell = (index, row, column, content) => {
72
+ const place = {
73
+ index,
74
+ column
75
+ };
76
+ const chosen = active?.index === index && active.column === column;
77
+ const editable = editor.editable(row, column);
78
+ const reachable = chosen || !active && index === 0 && column === columns[0];
79
+ return {
80
+ content: chosen && editing ? editor.render(row, column, close) : content,
81
+ chosen,
82
+ editing: chosen && editing,
83
+ editable,
84
+ props: {
85
+ "data-cell": placeId(place),
86
+ tabIndex: reachable ? 0 : -1,
87
+ "aria-selected": chosen,
88
+ onMouseDown: () => {
89
+ pressedChosen.current = chosen;
90
+ },
91
+ onFocus: (event) => {
92
+ if (event.target === event.currentTarget && !chosen) setActive(place);
93
+ },
94
+ onClick: (event) => {
95
+ if (!editable) return;
96
+ event.stopPropagation();
97
+ if (pressedChosen.current && !editing) choose(place, true);
98
+ },
99
+ onDoubleClick: (event) => {
100
+ if (!editable) return;
101
+ event.stopPropagation();
102
+ if (!(chosen && editing)) choose(place, true);
103
+ }
104
+ }
105
+ };
106
+ };
107
+ return {
108
+ cell,
109
+ onKeyDown
110
+ };
111
+ };
112
+ //#endregion
113
+ export { useCellEditing };
@@ -1,10 +1,13 @@
1
1
  import { createPageCache, pagesForRange } from "./page-cache.js";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ /** How many rows of `total` the page holds: a whole page, but for the last. */
4
+ const pageLength = (total, page) => Math.max(0, Math.min(40, total - page * 40));
3
5
  /**
4
6
  * Serves rows by absolute index. Mock sources compute rows (after `latencyMs`, or at once when 0);
5
- * live sources load pages through `loadRows`. Unloaded rows read as undefined, so fast scrolling shows skeletons.
7
+ * live sources load pages through `loadRows`. Unloaded rows read as undefined, so fast scrolling shows skeletons. A new
8
+ * `revision` loads the pages on screen again, showing the rows they held until the new ones arrive.
6
9
  */
7
- const usePagedRows = ({ total, rowAt, loadRows, latencyMs }, start, end) => {
10
+ const usePagedRows = ({ total, rowAt, loadRows, revision, latencyMs }, start, end) => {
8
11
  const cache = useMemo(() => createPageCache(40, 60), [rowAt, loadRows]);
9
12
  const [version, setVersion] = useState(0);
10
13
  const firstPage = Math.floor(start / 40);
@@ -13,7 +16,7 @@ const usePagedRows = ({ total, rowAt, loadRows, latencyMs }, start, end) => {
13
16
  useEffect(() => {
14
17
  if (synchronous) return;
15
18
  const pages = pagesForRange(firstPage * 40, lastPage * 40, 40).filter((page) => !cache.hasPage(page));
16
- const length = (page) => Math.max(0, Math.min(40, total - page * 40));
19
+ const length = (page) => pageLength(total, page);
17
20
  const store = (page, rows) => {
18
21
  cache.setPage(page, rows);
19
22
  setVersion((current) => current + 1);
@@ -39,6 +42,24 @@ const usePagedRows = ({ total, rowAt, loadRows, latencyMs }, start, end) => {
39
42
  loadRows,
40
43
  synchronous
41
44
  ]);
45
+ const shownRevision = useRef(revision);
46
+ useEffect(() => {
47
+ if (!loadRows || revision === shownRevision.current) return;
48
+ shownRevision.current = revision;
49
+ const shown = pagesForRange(firstPage * 40, lastPage * 40, 40);
50
+ cache.keepOnly(shown);
51
+ for (const page of shown.filter((page) => cache.hasPage(page))) loadRows(page * 40, pageLength(total, page)).then((rows) => {
52
+ cache.setPage(page, rows);
53
+ setVersion((current) => current + 1);
54
+ });
55
+ }, [
56
+ cache,
57
+ revision,
58
+ loadRows,
59
+ firstPage,
60
+ lastPage,
61
+ total
62
+ ]);
42
63
  return useCallback((index) => synchronous ? rowAt(index) : cache.get(index), [
43
64
  cache,
44
65
  synchronous,
package/dist/index.js CHANGED
@@ -37,14 +37,14 @@ import { Field } from "./record-view/field.js";
37
37
  import { Section } from "./record-view/section.js";
38
38
  import { Textarea } from "./primitives/textarea.js";
39
39
  import { AppLink, useAppAddress } from "./app/use-app-address.js";
40
+ import { useList } from "./data/use-list.js";
40
41
  import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
41
42
  import { useLayoutRecord } from "./app/pages/record-scope.js";
43
+ import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
42
44
  import { SignInPrompt } from "./auth/sign-in-prompt.js";
43
45
  import { useListDisplay } from "./app/displays/display-context.js";
44
46
  import { MenuSelect } from "./primitives/menu-select.js";
45
47
  import { Form } from "./app/create/form.js";
46
- import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
47
- import { useList } from "./data/use-list.js";
48
48
  import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
49
49
  import { App } from "./app/app.js";
50
50
  import { RecordLink } from "./app/displays/record-link.js";
@@ -12,6 +12,8 @@ const textEdited = /* @__PURE__ */ new Set([
12
12
  "country"
13
13
  ]);
14
14
  const isEditable = (field) => !field.readOnly && field.type !== "json" && field.type !== "uuid" && field.type !== "timestamp" && field.type !== "relation";
15
+ /** A field a record's form cannot leave empty: written through the API, never null and no boolean, which always holds a value. */
16
+ const isRequiredOnEdit = (field) => !field.readOnly && !field.nullable && field.type !== "boolean";
15
17
  /** The JSON value to send for an edited field: integers become numbers, empty optional fields become null. */
16
18
  const toPatchValue = (field, draft) => {
17
19
  if (field.type === "boolean") return Boolean(draft);
@@ -40,4 +42,4 @@ const differs = (field, draft, stored) => {
40
42
  return a !== b;
41
43
  };
42
44
  //#endregion
43
- export { differs, fromDraft, isEditable, toDraft, toFieldDraft, toPatchValue };
45
+ export { differs, fromDraft, isEditable, isRequiredOnEdit, toDraft, toFieldDraft, toPatchValue };
@@ -1,7 +1,7 @@
1
1
  import { listLabels, withLabels } from "./relation-labels.js";
2
2
  import { createWhere, printFilter } from "@protobase/schema";
3
3
  //#region src/live/related-column-rows.ts
4
- const where = createWhere();
4
+ const where$1 = createWhere();
5
5
  const and = (...parts) => {
6
6
  const given = parts.filter((part) => Boolean(part));
7
7
  return given.length === 1 ? given[0] : given.map((part) => `(${part})`).join(" AND ");
@@ -29,7 +29,7 @@ const linksOf = async (client, resources, column, owners) => {
29
29
  const key = through.key ?? via?.primaryKey[0];
30
30
  if (!via || key === void 0) throw new Error(`The related column ${column.name} names a join resource the app does not have: ${through.resource}`);
31
31
  return (await client.search(via.name, {
32
- filter: and(printFilter(where.in(through.field, owners)), through.filter),
32
+ filter: and(printFilter(where$1.in(through.field, owners)), through.filter),
33
33
  fields: [through.field, key],
34
34
  pageSize: 500
35
35
  })).items.flatMap((row) => {
@@ -58,7 +58,7 @@ const loadRelatedColumn = async ({ client, resources, views, load, owners }) =>
58
58
  ])];
59
59
  const labels = listLabels(resources, views, target, loaded);
60
60
  const page = await client.search(target.name, {
61
- filter: and(printFilter(where.in(column.field, values)), column.filter),
61
+ filter: and(printFilter(where$1.in(column.field, values)), column.filter),
62
62
  ...column.sort && { orderBy: column.sort },
63
63
  fields: loaded,
64
64
  pageSize: 500,
@@ -2,7 +2,7 @@ import { keys } from "../data/query-keys.js";
2
2
  import { labelFieldOf } from "./model-helpers.js";
3
3
  import { createWhere, printFilter } from "@protobase/schema";
4
4
  //#region src/live/relation-labels.ts
5
- const where = createWhere();
5
+ const where$1 = createWhere();
6
6
  /** Names for the given keys of a single-key resource, fetched in one list call and cached. */
7
7
  const fetchLabels = async (queryClient, client, target, ids, view) => {
8
8
  const keyField = target.primaryKey[0];
@@ -14,7 +14,7 @@ const fetchLabels = async (queryClient, client, target, ids, view) => {
14
14
  staleTime: 3e5,
15
15
  queryFn: async () => {
16
16
  return (await client.list(target.name, {
17
- filter: printFilter(where.in(keyField, unique)),
17
+ filter: printFilter(where$1.in(keyField, unique)),
18
18
  fields: [keyField, labelField],
19
19
  pageSize: unique.length
20
20
  })).items.map((item) => [String(item[keyField]), String(item[labelField])]);
@@ -6,7 +6,9 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
6
6
  //#region src/live/use-live-rows.ts
7
7
  /**
8
8
  * A grid row source backed by the API; `total` is the server's estimate from the first page. `page` loads one page of
9
- * the same rows by number, and `key` names them in the query cache, for list displays that page through them.
9
+ * the same rows by number, and `key` names them in the query cache, for list displays that page through them. Each
10
+ * fetch of the first page, as on a return to the window, makes every page of the list stale and gives the source a new
11
+ * `revision`, so the rows on screen are loaded again.
10
12
  */
11
13
  const useLiveRows = ({ resources, views, model, columns, filter, orderBy, related }) => {
12
14
  const client = useClient();
@@ -36,13 +38,24 @@ const useLiveRows = ({ resources, views, model, columns, filter, orderBy, relate
36
38
  ]);
37
39
  const first = useQuery({
38
40
  queryKey: [...keys.first(model.name, filter, orderBy), columnKey],
39
- queryFn: () => loader.page(0)
41
+ queryFn: async () => {
42
+ await queryClient.invalidateQueries({
43
+ queryKey: keys.pages(model.name, filter, orderBy),
44
+ refetchType: "none"
45
+ });
46
+ return loader.page(0);
47
+ }
40
48
  });
41
49
  return {
42
50
  source: useMemo(() => first.data ? {
43
51
  total: first.data.total,
44
- loadRows: loader.loadRows
45
- } : void 0, [first.data, loader]),
52
+ loadRows: loader.loadRows,
53
+ revision: first.dataUpdatedAt
54
+ } : void 0, [
55
+ first.data,
56
+ first.dataUpdatedAt,
57
+ loader
58
+ ]),
46
59
  error: first.error,
47
60
  isLoading: first.isLoading,
48
61
  page: loader.page,
@@ -4,7 +4,7 @@ import { useState } from "react";
4
4
  import { keepPreviousData, useQueries } from "@tanstack/react-query";
5
5
  import { createWhere, printFilter } from "@protobase/schema";
6
6
  //#region src/live/use-reference-search.ts
7
- const where = createWhere();
7
+ const where$1 = createWhere();
8
8
  /**
9
9
  * The search text typed into each facet's combobox and the records of its target resource that match it. A facet
10
10
  * without a target, or whose target has no search fields or a composite key, gets nothing: its options are filtered
@@ -29,7 +29,7 @@ const useReferenceSearch = (targets, views) => {
29
29
  const keyField = target.primaryKey[0];
30
30
  const labelField = labelFieldOf(target, views[target.name]);
31
31
  return (await client.list(target.name, {
32
- filter: printFilter(where.search(text)),
32
+ filter: printFilter(where$1.search(text)),
33
33
  fields: [keyField, labelField],
34
34
  pageSize: 20
35
35
  })).items.map((item) => [String(item[keyField]), String(item[labelField])]);
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.19.15";
2
+ var version = "0.20.0";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };