@protobase/ui 0.19.14 → 0.19.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.js +2 -2
- package/dist/app/create/create-page.js +3 -3
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/board-column.js +114 -0
- package/dist/app/displays/board-columns.js +45 -0
- package/dist/app/displays/board-display.js +96 -0
- package/dist/app/displays/board-dnd.js +32 -0
- package/dist/app/displays/list-cards.js +1 -1
- package/dist/app/displays/list-displays.js +13 -1
- package/dist/app/displays/map-display.js +1 -1
- package/dist/app/displays/record-card.js +18 -7
- package/dist/app/displays/record-link.js +1 -1
- package/dist/app/displays/use-board-column.js +23 -0
- package/dist/app/displays/use-board-move.js +60 -0
- package/dist/app/displays/virtual-cards.js +1 -0
- package/dist/app/embedded-list/embedded-list.js +10 -1
- package/dist/app/export/export-options.js +2 -2
- package/dist/app/field-editor.js +6 -6
- package/dist/app/gallery/gallery-section.js +1 -1
- package/dist/app/gallery/lightbox.js +1 -1
- package/dist/app/gallery/photo-details.js +3 -3
- package/dist/app/gallery/use-gallery-photos.js +2 -2
- package/dist/app/import/import-dialog.js +1 -1
- package/dist/app/import/lookups.js +2 -2
- package/dist/app/list-edit/cell-field-editor.js +61 -0
- package/dist/app/list-edit/use-list-cell-editor.js +30 -0
- package/dist/app/list-edit/use-list-edits.js +75 -0
- package/dist/app/list-page.js +31 -13
- package/dist/app/map/map-area.js +6 -6
- package/dist/app/pages/action-href.js +4 -2
- package/dist/app/pages/action-inputs/action-inputs-dialog.d.ts +13 -0
- package/dist/app/pages/action-inputs/action-inputs-dialog.js +111 -0
- package/dist/app/pages/action-inputs/input-answers.js +88 -0
- package/dist/app/pages/action-inputs/input-errors.d.ts +11 -0
- package/dist/app/pages/action-inputs/input-errors.js +25 -0
- package/dist/app/pages/action-prompt.js +29 -0
- package/dist/app/pages/blocks/action.js +8 -4
- package/dist/app/pages/blocks/card-row.js +1 -1
- package/dist/app/pages/blocks/link.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +5 -5
- package/dist/app/pages/blocks/record-card.js +1 -1
- package/dist/app/pages/blocks/stat.js +1 -1
- package/dist/app/pages/field-value.js +2 -2
- package/dist/app/pages/project-ui.d.ts +5 -0
- package/dist/app/pages/record-actions.js +8 -4
- package/dist/app/pages/use-run-action.js +20 -9
- package/dist/app/record-lines.js +1 -1
- package/dist/app/record-page.js +41 -27
- package/dist/app/record-preview.js +25 -0
- package/dist/app/record-row/record-row-body.js +3 -3
- package/dist/app/record-row/record-row.js +1 -0
- package/dist/app/record-row/saved-record-row.js +1 -1
- package/dist/app/related-dots/card-dots-lines.js +16 -0
- package/dist/app/related-dots/card-dots.js +34 -0
- package/dist/app/related-dots/dot-card.js +3 -3
- package/dist/app/related-dots/related-dots-columns.js +3 -1
- package/dist/app/related-dots/related-dots.js +22 -9
- package/dist/app/related-section.js +5 -5
- package/dist/app/relation-picker.js +2 -2
- package/dist/app/saved/saved-records.js +2 -2
- package/dist/app/search/search-model.js +2 -2
- package/dist/app/searches/saved-search-rows.js +1 -1
- package/dist/app/searches/saved-search.js +3 -3
- package/dist/app/searches/saved-searches.js +2 -2
- package/dist/app/searches/search-controls.js +1 -1
- package/dist/app/searches/search-diff.js +8 -8
- package/dist/app/searches/search-name-dialog.js +1 -1
- package/dist/app/searches/update-search-dialog.js +1 -1
- package/dist/app/searches/use-save-search.js +1 -1
- package/dist/app/sensitive-field.js +2 -2
- package/dist/app/use-nav-recent.js +1 -2
- package/dist/app/use-record-editor.js +1 -1
- package/dist/auth/sign-in-prompt.js +1 -1
- package/dist/data/api-provider.d.ts +2 -1
- package/dist/data/api-provider.js +12 -5
- package/dist/data/focus-refresh.js +44 -0
- package/dist/data/query-keys.js +13 -0
- package/dist/data/use-record.js +17 -2
- package/dist/data-grid/cell-moves.js +26 -0
- package/dist/data-grid/cells.js +1 -1
- package/dist/data-grid/data-grid.js +32 -7
- package/dist/data-grid/grid-header.js +1 -1
- package/dist/data-grid/grid-row.js +17 -13
- package/dist/data-grid/page-cache.js +3 -0
- package/dist/data-grid/use-cell-editing.js +113 -0
- package/dist/data-grid/use-paged-rows.js +25 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -4
- package/dist/live/columns-from-model.js +1 -1
- package/dist/live/field-values.js +3 -1
- package/dist/live/related-column-rows.js +3 -3
- package/dist/live/relation-labels.js +2 -2
- package/dist/live/use-live-rows.js +17 -4
- package/dist/live/use-reference-search.js +2 -2
- package/dist/markdown/markdown-editor.js +1 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-xQmLJM07.js +69 -0
- package/dist/shell/assets/index-CmJl5dIO.css +2 -0
- package/dist/shell/assets/{index-DF5WCYEC.js → index-CmmYBR7Y.js} +1 -1
- package/dist/shell/assets/{maplibre-DYbWlNbK.js → maplibre-Siom2Zwj.js} +2 -2
- package/dist/shell/assets/{markdown-content-o9fxfOx0.js → markdown-content-ChyGEubR.js} +1 -1
- package/dist/shell/assets/{record-map-CmVH3wtS.js → record-map-B_QnHF99.js} +1 -1
- package/dist/shell/assets/{records-map-XibwyiR4.js → records-map-CcHhEDBC.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-zRmreKZS.js +1 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-Bet1bgM_.js +0 -69
- package/dist/shell/assets/index-DEEIUcYb.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-DNhP23mn.js +0 -1
package/dist/data-grid/cells.js
CHANGED
|
@@ -4,10 +4,10 @@ import { Badge } from "../primitives/badge.js";
|
|
|
4
4
|
import { formatMoney } from "../format/money.js";
|
|
5
5
|
import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
6
6
|
import { formatDate, formatDateTime } from "../format/date.js";
|
|
7
|
+
import { userById } from "../mocks/users.js";
|
|
7
8
|
import { fileShown, shownImage } from "../live/file-values.js";
|
|
8
9
|
import { FileImage } from "../live/file-image.js";
|
|
9
10
|
import { valueColor } from "../live/value-color.js";
|
|
10
|
-
import { userById } from "../mocks/users.js";
|
|
11
11
|
import { Check, Paperclip } from "lucide-react";
|
|
12
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { enumLabel } from "@protobase/schema";
|
|
@@ -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:
|
|
83
|
+
row: patchedRow(index)
|
|
74
84
|
}));
|
|
75
85
|
const data = useMemo(() => loaded.flatMap(({ row }) => row ? [row] : []), [
|
|
76
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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(
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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) =>
|
|
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.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { useList } from "./data/use-list.js";
|
|
|
11
11
|
import { useRecord } from "./data/use-record.js";
|
|
12
12
|
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
13
13
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
14
|
+
import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
|
|
15
|
+
import { ActionInputProps } from "./app/pages/action-inputs/action-inputs-dialog.js";
|
|
14
16
|
import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
|
|
15
17
|
import { mergeUi } from "./app/pages/merge-ui.js";
|
|
16
18
|
import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
@@ -57,4 +59,4 @@ import { Field, FieldProps } from "./record-view/field.js";
|
|
|
57
59
|
import { Section } from "./record-view/section.js";
|
|
58
60
|
import { Form, FormProps } from "./app/create/form.js";
|
|
59
61
|
import { Spinner } from "./primitives/spinner.js";
|
|
60
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, type BackendData, type BackendRefresh, type BeforeSignInProps, Button, type ButtonProps, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, Spinner, type Step, StepList, type StepState, Textarea, type TextareaProps, buttonClass, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
|
62
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, type BackendData, type BackendRefresh, type BeforeSignInProps, Button, type ButtonProps, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, Spinner, type Step, StepList, type StepState, Textarea, type TextareaProps, buttonClass, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -32,12 +32,13 @@ import { AssistantDock } from "./assistant-dock/assistant-dock.js";
|
|
|
32
32
|
import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
33
33
|
import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
|
|
34
34
|
import { useMeta } from "./data/use-meta.js";
|
|
35
|
-
import {
|
|
36
|
-
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
35
|
+
import { ResponsiveDialog } from "./primitives/responsive-dialog.js";
|
|
37
36
|
import { Field } from "./record-view/field.js";
|
|
38
37
|
import { Section } from "./record-view/section.js";
|
|
39
38
|
import { Textarea } from "./primitives/textarea.js";
|
|
40
|
-
import {
|
|
39
|
+
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
40
|
+
import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
|
|
41
|
+
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
41
42
|
import { SignInPrompt } from "./auth/sign-in-prompt.js";
|
|
42
43
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
43
44
|
import { MenuSelect } from "./primitives/menu-select.js";
|
|
@@ -48,4 +49,4 @@ import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
|
48
49
|
import { App } from "./app/app.js";
|
|
49
50
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
50
51
|
import { Choices } from "./primitives/choices.js";
|
|
51
|
-
export { ActionCard, ApiProvider, App, AppLink, AssistantDock, AuthLayout, Button, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, MenuSelect, OtpInput, PartView, RecordLink, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StepList, Textarea, buttonClass, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
|
52
|
+
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, Button, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, MenuSelect, OtpInput, PartView, RecordLink, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StepList, Textarea, buttonClass, definePlugin, defineUi, diffStats, isConflict, mergeUi, parseDiff, pluginApi, protobaseVersion, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, usePlugin, useRecord, useResourceClient, useSeries, useUpdateRecord };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { humanize } from "./naming.js";
|
|
2
|
-
import { markdownExcerpt } from "../markdown/markdown-excerpt.js";
|
|
3
2
|
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
3
|
+
import { markdownExcerpt } from "../markdown/markdown-excerpt.js";
|
|
4
4
|
import { enumLabel } from "@protobase/schema";
|
|
5
5
|
//#region src/live/columns-from-model.ts
|
|
6
6
|
const tones = {
|
|
@@ -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: () =>
|
|
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
|
-
|
|
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])]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Segmented } from "../primitives/segmented.js";
|
|
2
|
-
import { Markdown } from "./markdown.js";
|
|
3
2
|
import { Textarea } from "../primitives/textarea.js";
|
|
3
|
+
import { Markdown } from "./markdown.js";
|
|
4
4
|
import { useState } from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
//#region src/markdown/markdown-editor.tsx
|
package/dist/package.js
CHANGED