@protobase/ui 0.14.0 → 0.14.2
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/current-address.js +19 -1
- package/dist/app/displays/card-carousel.js +47 -39
- package/dist/app/displays/card-image.js +25 -0
- package/dist/app/displays/card-photos.js +11 -15
- package/dist/app/displays/cards-display.js +2 -1
- package/dist/app/displays/list-cards.js +2 -2
- package/dist/app/displays/list-displays.d.ts +2 -0
- package/dist/app/displays/list-displays.js +9 -3
- package/dist/app/displays/map-display.js +5 -4
- package/dist/app/displays/record-card.js +28 -34
- package/dist/app/displays/use-map-records.js +8 -6
- package/dist/app/displays/virtual-cards.js +43 -65
- package/dist/app/embedded-list/embedded-columns.js +1 -1
- package/dist/app/embedded-list/use-embedded-rows.js +11 -6
- package/dist/app/list-page.js +53 -13
- package/dist/app/menu-icons.js +3 -1
- package/dist/app/named-icon.js +5 -3
- package/dist/app/nav-from-meta.js +21 -1
- package/dist/app/pages/blocks/card-row.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +1 -1
- package/dist/app/pages/blocks/record-card.js +1 -1
- package/dist/app/pages/blocks/show.js +1 -1
- package/dist/app/pages/blocks/stat.js +1 -1
- package/dist/app/pages/use-run-action.js +1 -1
- package/dist/app/saved/favorite-button.js +49 -0
- package/dist/app/saved/saved-list.js +43 -0
- package/dist/app/saved/saved-records.js +38 -0
- package/dist/app/saved/use-favorite.js +62 -0
- package/dist/app/saved/use-saved-records.js +36 -0
- package/dist/app/use-app-address.js +5 -2
- package/dist/app-shell/app-shell.js +54 -38
- package/dist/app-shell/bottom-bar.d.ts +1 -0
- package/dist/app-shell/bottom-bar.js +3 -2
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/page-scroll.d.ts +1 -0
- package/dist/app-shell/page-scroll.js +56 -0
- package/dist/app-shell/sidebar.js +2 -2
- package/dist/app-shell/top-bar.js +3 -2
- package/dist/auth/sign-in-prompt.d.ts +22 -0
- package/dist/auth/sign-in-prompt.js +84 -0
- package/dist/data/api-provider.d.ts +2 -0
- package/dist/data/meta-data.d.ts +3 -1
- package/dist/data/query-keys.js +0 -5
- package/dist/data/use-meta.js +1 -0
- package/dist/data-grid/data-grid.js +92 -85
- package/dist/data-grid/geometry.js +26 -6
- package/dist/data-grid/grid-footer.js +5 -12
- package/dist/data-grid/scroll-parent.js +35 -0
- package/dist/data-grid/use-virtual-rows.js +123 -18
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/live/grid-loader.js +8 -3
- package/dist/live/reference-options.js +2 -2
- package/dist/live/relation-labels.js +16 -18
- package/dist/live/row-photos.js +18 -0
- package/dist/live/use-filter-config.js +7 -2
- package/dist/package.js +1 -1
- package/dist/primitives/button.js +7 -2
- package/dist/primitives/dialog.js +1 -1
- package/dist/primitives/responsive-dialog.d.ts +17 -0
- package/dist/primitives/responsive-dialog.js +44 -0
- package/dist/shell/assets/app-B2GAOqtJ.js +66 -0
- package/dist/shell/assets/index-BXYHpFoe.css +2 -0
- package/dist/shell/assets/{index-CwUFCDf9.js → index-DTbA97zz.js} +1 -1
- package/dist/shell/assets/{maplibre-gl-QGc1hPKt.js → maplibre-gl-CSTJJJQT.js} +1 -1
- package/dist/shell/assets/{record-map-D5vqYcYw.js → record-map-CeTF-ArN.js} +2 -2
- package/dist/shell/assets/{records-map-DqqaK1rm.js → records-map-Cl0Ofb1s.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-4J-P80A-.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/data-grid/use-grid-scroll.js +0 -87
- package/dist/shell/assets/app-DtPlcCA3.js +0 -66
- package/dist/shell/assets/index-CYWfU_c1.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-0ozDlcDr.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/data-grid/geometry.ts
|
|
2
2
|
/** Browsers cap element height (about 17M px in the strictest); stay well below it. */
|
|
3
3
|
const maxContentHeight = 1e7;
|
|
4
|
+
/** The list's height in the page, `contentHeight`: its true height, or the cap when the rows need more. */
|
|
4
5
|
const gridGeometry = (total, rowHeight, viewportHeight) => {
|
|
5
6
|
const trueHeight = total * rowHeight;
|
|
6
7
|
const contentHeight = Math.min(trueHeight, maxContentHeight);
|
|
@@ -13,13 +14,32 @@ const gridGeometry = (total, rowHeight, viewportHeight) => {
|
|
|
13
14
|
maxScroll: Math.max(0, contentHeight - viewportHeight)
|
|
14
15
|
};
|
|
15
16
|
};
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The offset into the rows (the virtual offset) shown at `position`, how far the top of the view is into the list.
|
|
19
|
+
* They are the same unless the list is capped: then the capped range maps onto all the rows, and a position before or
|
|
20
|
+
* after it moves one to one.
|
|
21
|
+
*/
|
|
22
|
+
const topFromScroll = (geometry, position) => {
|
|
23
|
+
if (!geometry.scaled || position <= 0) return position;
|
|
24
|
+
if (position >= geometry.maxScroll) return geometry.maxTop + position - geometry.maxScroll;
|
|
25
|
+
return position / geometry.maxScroll * geometry.maxTop;
|
|
26
|
+
};
|
|
27
|
+
const scrollFromTop = (geometry, top) => {
|
|
28
|
+
if (!geometry.scaled || top <= 0) return top;
|
|
29
|
+
if (top >= geometry.maxTop) return geometry.maxScroll + top - geometry.maxTop;
|
|
30
|
+
return top / geometry.maxTop * geometry.maxScroll;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Where row `index` is drawn in the list when the view at `position` shows the rows from `top`: its own place, or in a
|
|
34
|
+
* capped list its place relative to the view, so that it scrolls with the page until the next draw.
|
|
35
|
+
*/
|
|
36
|
+
const rowOffset = (geometry, index, top, position) => {
|
|
37
|
+
const offset = index * geometry.rowHeight;
|
|
38
|
+
return geometry.scaled ? offset - top + position : offset;
|
|
39
|
+
};
|
|
20
40
|
/** The rows drawn for a scroll offset: those in a viewport of `height`, and `overscan` more on each side. */
|
|
21
41
|
const rowsInView = (total, top, rowHeight, height, overscan) => {
|
|
22
|
-
const first = Math.max(0, Math.floor(top / rowHeight) - overscan);
|
|
42
|
+
const first = Math.min(total, Math.max(0, Math.floor(top / rowHeight) - overscan));
|
|
23
43
|
const last = Math.min(total - 1, Math.floor((top + height) / rowHeight) + overscan);
|
|
24
44
|
return {
|
|
25
45
|
first,
|
|
@@ -27,4 +47,4 @@ const rowsInView = (total, top, rowHeight, height, overscan) => {
|
|
|
27
47
|
};
|
|
28
48
|
};
|
|
29
49
|
//#endregion
|
|
30
|
-
export {
|
|
50
|
+
export { gridGeometry, maxContentHeight, rowOffset, rowsInView, scrollFromTop, topFromScroll };
|
|
@@ -2,9 +2,10 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { formatApproxRows, formatInt } from "../format/number.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/data-grid/grid-footer.tsx
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** How many rows match and, with rows selected, how many and what can be done with them. */
|
|
6
|
+
const GridFooter = ({ total, selectedCount, bulkActions = [], className }) => /* @__PURE__ */ jsx("div", {
|
|
7
|
+
className: cn("flex h-9 shrink-0 items-center border-t bg-surface px-3 text-xs text-muted-foreground", className),
|
|
8
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
8
9
|
className: "flex items-center tabular-nums",
|
|
9
10
|
children: [
|
|
10
11
|
formatApproxRows(total),
|
|
@@ -19,15 +20,7 @@ const GridFooter = ({ total, first, last, selectedCount, bulkActions = [] }) =>
|
|
|
19
20
|
children: action.label
|
|
20
21
|
}, action.label))
|
|
21
22
|
]
|
|
22
|
-
})
|
|
23
|
-
className: "tabular-nums",
|
|
24
|
-
children: [
|
|
25
|
-
"Rows ",
|
|
26
|
-
formatInt(first),
|
|
27
|
-
" – ",
|
|
28
|
-
formatInt(last)
|
|
29
|
-
]
|
|
30
|
-
})]
|
|
23
|
+
})
|
|
31
24
|
});
|
|
32
25
|
//#endregion
|
|
33
26
|
export { GridFooter };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/data-grid/scroll-parent.ts
|
|
2
|
+
/**
|
|
3
|
+
* The element that scrolls `element` up and down: its nearest ancestor that scrolls vertically, or the page's
|
|
4
|
+
* scrolling element when none does.
|
|
5
|
+
*/
|
|
6
|
+
const scrollParent = (element) => {
|
|
7
|
+
const page = document.scrollingElement ?? document.documentElement;
|
|
8
|
+
for (let parent = element.parentElement; parent && parent !== page && parent !== document.body; parent = parent.parentElement) if (/auto|scroll/.test(getComputedStyle(parent).overflowY)) return parent;
|
|
9
|
+
return page;
|
|
10
|
+
};
|
|
11
|
+
const isPage = (scroller) => scroller === (document.scrollingElement ?? document.documentElement);
|
|
12
|
+
/** Where scroll events of `scroller` are sent: the window for the page. */
|
|
13
|
+
const scrollEvents = (scroller) => isPage(scroller) ? window : scroller;
|
|
14
|
+
const padding = (value) => Number.parseFloat(value) || 0;
|
|
15
|
+
/**
|
|
16
|
+
* The part of `scroller` content shows in, in viewport coordinates: its box less its scroll padding, which is where
|
|
17
|
+
* sticky bars over it say they are.
|
|
18
|
+
*/
|
|
19
|
+
const visibleArea = (scroller) => {
|
|
20
|
+
const style = getComputedStyle(scroller);
|
|
21
|
+
const box = isPage(scroller) ? {
|
|
22
|
+
top: 0,
|
|
23
|
+
height: scroller.clientHeight
|
|
24
|
+
} : {
|
|
25
|
+
top: scroller.getBoundingClientRect().top + scroller.clientTop,
|
|
26
|
+
height: scroller.clientHeight
|
|
27
|
+
};
|
|
28
|
+
const top = padding(style.scrollPaddingTop);
|
|
29
|
+
return {
|
|
30
|
+
top: box.top + top,
|
|
31
|
+
height: Math.max(0, box.height - top - padding(style.scrollPaddingBottom))
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { scrollEvents, scrollParent, visibleArea };
|
|
@@ -1,30 +1,135 @@
|
|
|
1
|
-
import { rowsInView } from "./geometry.js";
|
|
2
|
-
import {
|
|
1
|
+
import { gridGeometry, rowOffset, rowsInView, scrollFromTop, topFromScroll } from "./geometry.js";
|
|
2
|
+
import { scrollEvents, scrollParent, visibleArea } from "./scroll-parent.js";
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
3
4
|
//#region src/data-grid/use-virtual-rows.ts
|
|
4
5
|
const overscan = 6;
|
|
6
|
+
const viewRows = (view, total, rowHeight) => {
|
|
7
|
+
const geometry = gridGeometry(total, rowHeight, view.height);
|
|
8
|
+
const top = topFromScroll(geometry, view.position);
|
|
9
|
+
return {
|
|
10
|
+
geometry,
|
|
11
|
+
top,
|
|
12
|
+
topIndex: Math.floor(Math.max(0, top) / rowHeight),
|
|
13
|
+
...rowsInView(total, top, rowHeight, view.height, overscan)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
5
16
|
/**
|
|
6
|
-
* Rows of a fixed height
|
|
7
|
-
*
|
|
17
|
+
* Rows of a fixed height in a list as tall as all of them, scrolled natively by whatever scrolls it: the nearest
|
|
18
|
+
* scrolling ancestor, or the page. Only the rows in view, and `overscan` more on each side, are drawn: `ref` goes on the
|
|
19
|
+
* list's element, `geometry.contentHeight` tall, and each drawn row sits at `offset` from its top. Nothing listens for
|
|
20
|
+
* wheel or touch: the list follows the scroll position, and draws again only when other rows come into view.
|
|
8
21
|
* `first` and `last` are the indexes of the drawn rows, overscan included.
|
|
9
22
|
*/
|
|
10
|
-
const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle }) => {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
23
|
+
const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle, ready = true }) => {
|
|
24
|
+
const ref = useRef(null);
|
|
25
|
+
const scroller = useRef(null);
|
|
26
|
+
const [view, setView] = useState({
|
|
27
|
+
position: 0,
|
|
28
|
+
height: 0
|
|
29
|
+
});
|
|
30
|
+
const options = useRef({
|
|
31
|
+
total,
|
|
32
|
+
rowHeight,
|
|
33
|
+
initialIndex,
|
|
34
|
+
inset,
|
|
35
|
+
ready
|
|
36
|
+
});
|
|
37
|
+
options.current = {
|
|
38
|
+
total,
|
|
39
|
+
rowHeight,
|
|
40
|
+
initialIndex,
|
|
41
|
+
inset,
|
|
42
|
+
ready
|
|
43
|
+
};
|
|
44
|
+
const read = useCallback(() => {
|
|
45
|
+
const content = ref.current;
|
|
46
|
+
if (!content || !scroller.current) return void 0;
|
|
47
|
+
const area = visibleArea(scroller.current);
|
|
48
|
+
const hidden = options.current.inset;
|
|
49
|
+
return {
|
|
50
|
+
position: area.top + hidden - content.getBoundingClientRect().top,
|
|
51
|
+
height: Math.max(0, area.height - hidden)
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
const shown = useRef(view);
|
|
55
|
+
shown.current = view;
|
|
56
|
+
const measure = useCallback(() => {
|
|
57
|
+
const next = read();
|
|
58
|
+
if (!next) return;
|
|
59
|
+
const current = shown.current;
|
|
60
|
+
const { total, rowHeight } = options.current;
|
|
61
|
+
const before = viewRows(current, total, rowHeight);
|
|
62
|
+
const after = viewRows(next, total, rowHeight);
|
|
63
|
+
const sameRows = !after.geometry.scaled && before.first === after.first && before.last === after.last && before.topIndex === after.topIndex;
|
|
64
|
+
if (current.height === next.height && (current.position === next.position || sameRows)) return;
|
|
65
|
+
shown.current = next;
|
|
66
|
+
setView(next);
|
|
67
|
+
}, [read]);
|
|
68
|
+
const start = useRef({
|
|
69
|
+
pending: true,
|
|
70
|
+
laidOut: false
|
|
71
|
+
});
|
|
72
|
+
const begin = useCallback(() => {
|
|
73
|
+
if (!start.current.pending || !start.current.laidOut || !options.current.ready) return;
|
|
74
|
+
const view = read();
|
|
75
|
+
const parent = scroller.current;
|
|
76
|
+
if (!view || !parent) return;
|
|
77
|
+
start.current.pending = false;
|
|
78
|
+
const { total, rowHeight, initialIndex } = options.current;
|
|
79
|
+
if (initialIndex > 0) parent.scrollTop += scrollFromTop(gridGeometry(total, rowHeight, view.height), initialIndex * rowHeight) - view.position;
|
|
80
|
+
else if (view.position > 0) parent.scrollTop = 0;
|
|
81
|
+
}, [read]);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
const content = ref.current;
|
|
84
|
+
if (!content) return;
|
|
85
|
+
const parent = scrollParent(content);
|
|
86
|
+
scroller.current = parent;
|
|
87
|
+
const events = scrollEvents(parent);
|
|
88
|
+
events.addEventListener("scroll", measure, { passive: true });
|
|
89
|
+
window.addEventListener("resize", measure, { passive: true });
|
|
90
|
+
const observer = new ResizeObserver(() => {
|
|
91
|
+
start.current.laidOut = true;
|
|
92
|
+
begin();
|
|
93
|
+
measure();
|
|
94
|
+
});
|
|
95
|
+
observer.observe(parent);
|
|
96
|
+
observer.observe(content);
|
|
97
|
+
measure();
|
|
98
|
+
return () => {
|
|
99
|
+
events.removeEventListener("scroll", measure);
|
|
100
|
+
window.removeEventListener("resize", measure);
|
|
101
|
+
observer.disconnect();
|
|
102
|
+
scroller.current = null;
|
|
103
|
+
};
|
|
104
|
+
}, [measure, begin]);
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
begin();
|
|
107
|
+
measure();
|
|
108
|
+
});
|
|
109
|
+
/** Scrolls to a virtual offset, for a layout change that keeps a row in view. */
|
|
110
|
+
const scrollTo = useCallback((top) => {
|
|
111
|
+
const current = read();
|
|
112
|
+
if (!current || !scroller.current) return;
|
|
113
|
+
const geometry = gridGeometry(options.current.total, options.current.rowHeight, current.height);
|
|
114
|
+
scroller.current.scrollTop += scrollFromTop(geometry, top) - current.position;
|
|
115
|
+
measure();
|
|
116
|
+
}, [read, measure]);
|
|
117
|
+
const drawn = viewRows(view, total, rowHeight);
|
|
118
|
+
const top = settle ? settle(drawn.top) : drawn.top;
|
|
119
|
+
const { first, last } = settle ? rowsInView(total, top, rowHeight, view.height, overscan) : drawn;
|
|
120
|
+
const rows = Array.from({ length: last - first + 1 }, (_, offset) => ({
|
|
121
|
+
index: first + offset,
|
|
122
|
+
offset: rowOffset(drawn.geometry, first + offset, top, view.position)
|
|
123
|
+
}));
|
|
15
124
|
return {
|
|
16
125
|
ref,
|
|
17
|
-
|
|
18
|
-
viewport,
|
|
126
|
+
height: view.height,
|
|
19
127
|
top,
|
|
20
|
-
geometry,
|
|
21
|
-
|
|
22
|
-
rows: Array.from({ length: last - first + 1 }, (_, offset) => ({
|
|
23
|
-
index: first + offset,
|
|
24
|
-
offset: (first + offset) * rowHeight - top
|
|
25
|
-
})),
|
|
128
|
+
geometry: drawn.geometry,
|
|
129
|
+
rows,
|
|
26
130
|
first,
|
|
27
|
-
last
|
|
131
|
+
last,
|
|
132
|
+
scrollTo
|
|
28
133
|
};
|
|
29
134
|
};
|
|
30
135
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { ListDisplayRow, RecordCardProps } from "./app/displays/list-displays.js
|
|
|
20
20
|
import { ListDisplay, useListDisplay } from "./app/displays/display-context.js";
|
|
21
21
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
22
22
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
23
|
+
import { SignInPrompt, SignInPromptProps } from "./auth/sign-in-prompt.js";
|
|
24
|
+
import { ResponsiveDialog, ResponsiveDialogProps } from "./primitives/responsive-dialog.js";
|
|
23
25
|
import { AssistantDock, AssistantDockProps } from "./assistant-dock/assistant-dock.js";
|
|
24
26
|
import { PartView } from "./assistant-dock/part-view.js";
|
|
25
27
|
import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
@@ -39,4 +41,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
|
|
|
39
41
|
import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
|
|
40
42
|
import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
|
|
41
43
|
import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
|
|
42
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, 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 };
|
|
44
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type PluginInfo, type ProjectUi, type RecordCardProps, RecordLink, ResponsiveDialog, type ResponsiveDialogProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type Step, StepList, type StepState, 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
|
@@ -19,6 +19,8 @@ import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
|
19
19
|
import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
|
|
20
20
|
import { useMeta } from "./data/use-meta.js";
|
|
21
21
|
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
22
|
+
import { ResponsiveDialog } from "./primitives/responsive-dialog.js";
|
|
23
|
+
import { SignInPrompt } from "./auth/sign-in-prompt.js";
|
|
22
24
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
23
25
|
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
24
26
|
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
@@ -30,4 +32,4 @@ import { useList } from "./data/use-list.js";
|
|
|
30
32
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
31
33
|
import { App } from "./app/app.js";
|
|
32
34
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
33
|
-
export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, SidePanel, StepList, 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 };
|
|
35
|
+
export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, ResponsiveDialog, SidePanel, SignInPrompt, StepList, 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/live/grid-loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { keys } from "../data/query-keys.js";
|
|
2
2
|
import { recordId } from "./model-helpers.js";
|
|
3
|
-
import { withLabels } from "./relation-labels.js";
|
|
3
|
+
import { listLabels, withLabels } from "./relation-labels.js";
|
|
4
|
+
import { listPhotos, withPhotos } from "./row-photos.js";
|
|
4
5
|
//#region src/live/grid-loader.ts
|
|
5
6
|
const exactBelow = 5e3;
|
|
6
7
|
/**
|
|
@@ -10,6 +11,8 @@ const exactBelow = 5e3;
|
|
|
10
11
|
*/
|
|
11
12
|
const createGridLoader = ({ queryClient, client, resources, views, model, columns, filter, orderBy }) => {
|
|
12
13
|
const pageKey = (number) => keys.page(model.name, filter, orderBy, 40, number);
|
|
14
|
+
const labels = listLabels(resources, views, model, columns);
|
|
15
|
+
const photos = listPhotos(views[model.name]);
|
|
13
16
|
/** The server's total is a planner estimate, off by a lot for small results; settle those exactly. */
|
|
14
17
|
const firstPageTotal = async (result) => {
|
|
15
18
|
if (result.nextPageToken === "") return result.items.length;
|
|
@@ -40,7 +43,9 @@ const createGridLoader = ({ queryClient, client, resources, views, model, column
|
|
|
40
43
|
const params = {
|
|
41
44
|
filter,
|
|
42
45
|
orderBy,
|
|
43
|
-
pageSize: 40
|
|
46
|
+
pageSize: 40,
|
|
47
|
+
labels,
|
|
48
|
+
photos
|
|
44
49
|
};
|
|
45
50
|
const result = token !== void 0 ? await client.list(model.name, {
|
|
46
51
|
...params,
|
|
@@ -56,7 +61,7 @@ const createGridLoader = ({ queryClient, client, resources, views, model, column
|
|
|
56
61
|
};
|
|
57
62
|
const ids = result.items.map((record) => recordId(model, record));
|
|
58
63
|
return {
|
|
59
|
-
rows:
|
|
64
|
+
rows: withLabels(resources, model, columns, withPhotos(model, result.items, result.photos), result.labels).map((record, index) => ({
|
|
60
65
|
...record,
|
|
61
66
|
id: ids[index]
|
|
62
67
|
})),
|
|
@@ -8,7 +8,7 @@ const matchOptions = (matches, counted) => {
|
|
|
8
8
|
...counts.get(value) !== void 0 && { count: counts.get(value) }
|
|
9
9
|
}));
|
|
10
10
|
};
|
|
11
|
-
/** The keys a facet needs names for: the values
|
|
12
|
-
const labelKeys = (counted, chosen) => [
|
|
11
|
+
/** The keys a facet still needs names for: the counted values come named, but a chosen one may lie outside them. */
|
|
12
|
+
const labelKeys = (counted, chosen) => [...new Set(chosen.filter((value) => !counted.includes(value)))];
|
|
13
13
|
//#endregion
|
|
14
14
|
export { labelKeys, matchOptions };
|
|
@@ -22,32 +22,30 @@ const fetchLabels = async (queryClient, client, target, ids, view) => {
|
|
|
22
22
|
});
|
|
23
23
|
return new Map(entries);
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}));
|
|
39
|
-
const labels = new Map(resolved);
|
|
25
|
+
const relationColumns = (resources, model, columns) => columns.flatMap((name) => {
|
|
26
|
+
const field = model.fields[name];
|
|
27
|
+
const target = field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
|
|
28
|
+
return target ? [{
|
|
29
|
+
name,
|
|
30
|
+
target
|
|
31
|
+
}] : [];
|
|
32
|
+
});
|
|
33
|
+
/** What a list call asks `labels` for: each relation among `columns` to a single-key resource, by the field that names its records. */
|
|
34
|
+
const listLabels = (resources, views, model, columns) => Object.fromEntries(relationColumns(resources, model, columns).filter(({ target }) => target.primaryKey.length === 1).map(({ name, target }) => [name, labelFieldOf(target, views[target.name])]));
|
|
35
|
+
/** Replaces each relation value among `columns` with `{ id, name }`, named by the list call's `labels`, else by its key. */
|
|
36
|
+
const withLabels = (resources, model, columns, rows, labels = {}) => {
|
|
37
|
+
const relations = relationColumns(resources, model, columns);
|
|
40
38
|
return rows.map((row) => {
|
|
41
39
|
const next = { ...row };
|
|
42
|
-
for (const
|
|
40
|
+
for (const { name } of relations) {
|
|
43
41
|
if (row[name] == null) continue;
|
|
44
42
|
next[name] = {
|
|
45
43
|
id: row[name],
|
|
46
|
-
name:
|
|
44
|
+
name: labels[name]?.[String(row[name])] ?? String(row[name])
|
|
47
45
|
};
|
|
48
46
|
}
|
|
49
47
|
return next;
|
|
50
48
|
});
|
|
51
49
|
};
|
|
52
50
|
//#endregion
|
|
53
|
-
export { fetchLabels, withLabels };
|
|
51
|
+
export { fetchLabels, listLabels, withLabels };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/live/row-photos.ts
|
|
2
|
+
/** Where a loaded row keeps its photos, beside its fields, as it keeps its `id`. */
|
|
3
|
+
const photosKey = "$photos";
|
|
4
|
+
/** The photos a list's rows load with: those its cards displays name, which the server checks they all share. */
|
|
5
|
+
const listPhotos = (view) => {
|
|
6
|
+
for (const display of view?.list?.displays ?? []) if (display.kind === "cards" && display.photos) return display.photos;
|
|
7
|
+
};
|
|
8
|
+
/** The rows, each with its record's photos from a list call's `photos`, when the call asked for them; none for a record it left out. */
|
|
9
|
+
const withPhotos = (model, rows, photos) => {
|
|
10
|
+
if (photos === void 0 || model.primaryKey.length !== 1) return rows;
|
|
11
|
+
const key = model.primaryKey[0];
|
|
12
|
+
return rows.map((row) => ({
|
|
13
|
+
...row,
|
|
14
|
+
[photosKey]: photos[String(row[key])] ?? []
|
|
15
|
+
}));
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { listPhotos, photosKey, withPhotos };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatMoney } from "../format/money.js";
|
|
2
2
|
import { keys } from "../data/query-keys.js";
|
|
3
3
|
import { useClient } from "../data/api-provider.js";
|
|
4
|
+
import { labelFieldOf } from "./model-helpers.js";
|
|
4
5
|
import { humanize } from "./naming.js";
|
|
5
6
|
import { fetchLabels } from "./relation-labels.js";
|
|
6
7
|
import { setRange } from "../filter-panel/filter-state.js";
|
|
@@ -37,11 +38,14 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
37
38
|
[group]: []
|
|
38
39
|
}
|
|
39
40
|
}, extras);
|
|
41
|
+
const target = targetOf(field);
|
|
42
|
+
const label = target?.primaryKey.length === 1 ? labelFieldOf(target, views[target.name]) : void 0;
|
|
40
43
|
return {
|
|
41
44
|
queryKey: keys.facets(model.name, field, filter),
|
|
42
45
|
queryFn: () => client.facets(model.name, field, {
|
|
43
46
|
filter,
|
|
44
|
-
limit: 50
|
|
47
|
+
limit: 50,
|
|
48
|
+
...label && { label }
|
|
45
49
|
})
|
|
46
50
|
};
|
|
47
51
|
}) });
|
|
@@ -84,7 +88,8 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
const field = model.fields[group.id];
|
|
87
|
-
const
|
|
91
|
+
const named = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.label === void 0 ? [] : [[String(facet.value), facet.label]]);
|
|
92
|
+
const names = new Map([...named, ...labelQueries[index]?.data ?? []]);
|
|
88
93
|
const values = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.value === null ? [] : [{
|
|
89
94
|
value: String(facet.value),
|
|
90
95
|
label: field?.type === "enum" ? enumLabel(String(facet.value), view?.fields[group.id]?.valueLabels) : names.get(String(facet.value)) ?? String(facet.value),
|
package/dist/package.js
CHANGED
|
@@ -12,12 +12,17 @@ const sizes = {
|
|
|
12
12
|
sm: "h-7 px-2.5 text-xs gap-1.5",
|
|
13
13
|
md: "h-8 px-3 text-[13px] gap-2"
|
|
14
14
|
};
|
|
15
|
+
/** A button's classes, for a link that looks like one. */
|
|
16
|
+
const buttonClass = ({ variant = "secondary", size = "md" } = {}) => cn("inline-flex shrink-0 items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-60 disabled:cursor-default", variants[variant], sizes[size]);
|
|
15
17
|
const Button = ({ variant = "secondary", size = "md", loading, className, children, disabled, ...rest }) => /* @__PURE__ */ jsxs("button", {
|
|
16
18
|
type: "button",
|
|
17
19
|
disabled: disabled || loading,
|
|
18
|
-
className: cn(
|
|
20
|
+
className: cn(buttonClass({
|
|
21
|
+
variant,
|
|
22
|
+
size
|
|
23
|
+
}), className),
|
|
19
24
|
...rest,
|
|
20
25
|
children: [loading && /* @__PURE__ */ jsx(Spinner, { className: "size-3.5" }), children]
|
|
21
26
|
});
|
|
22
27
|
//#endregion
|
|
23
|
-
export { Button };
|
|
28
|
+
export { Button, buttonClass };
|
|
@@ -30,7 +30,7 @@ const Dialog = ({ title, description, onClose, actions, children }) => {
|
|
|
30
30
|
children
|
|
31
31
|
}),
|
|
32
32
|
/* @__PURE__ */ jsx("div", {
|
|
33
|
-
className: "mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end [
|
|
33
|
+
className: "mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end [&>*]:min-h-11 sm:[&>*]:min-h-0",
|
|
34
34
|
children: actions
|
|
35
35
|
})
|
|
36
36
|
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/primitives/responsive-dialog.d.ts
|
|
3
|
+
export type ResponsiveDialogProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
open: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Buttons: right-aligned in the modal, stacked full width at the bottom of the sheet. */
|
|
9
|
+
actions: ReactNode;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A modal centred on the screen from the `md` breakpoint up, and a sheet rising from the bottom on a phone, switching as
|
|
14
|
+
* the viewport does. Both close on Escape and on a press outside, and keep focus inside while open.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ResponsiveDialog: ({ title, description, open, onClose, actions, children }: ResponsiveDialogProps) => import("react").JSX.Element | null;
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
2
|
+
import { Dialog } from "./dialog.js";
|
|
3
|
+
import { Sheet } from "./sheet.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/primitives/responsive-dialog.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A modal centred on the screen from the `md` breakpoint up, and a sheet rising from the bottom on a phone, switching as
|
|
8
|
+
* the viewport does. Both close on Escape and on a press outside, and keep focus inside while open.
|
|
9
|
+
*/
|
|
10
|
+
const ResponsiveDialog = ({ title, description, open, onClose, actions, children }) => {
|
|
11
|
+
const desktop = useIsDesktop();
|
|
12
|
+
if (!open) return null;
|
|
13
|
+
if (desktop) return /* @__PURE__ */ jsx(Dialog, {
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
onClose,
|
|
17
|
+
actions,
|
|
18
|
+
children
|
|
19
|
+
});
|
|
20
|
+
return /* @__PURE__ */ jsx(Sheet, {
|
|
21
|
+
title,
|
|
22
|
+
open: true,
|
|
23
|
+
onClose,
|
|
24
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
25
|
+
className: "pt-3 text-[13px]",
|
|
26
|
+
children: [
|
|
27
|
+
description && /* @__PURE__ */ jsx("p", {
|
|
28
|
+
className: "text-muted-foreground",
|
|
29
|
+
children: description
|
|
30
|
+
}),
|
|
31
|
+
children && /* @__PURE__ */ jsx("div", {
|
|
32
|
+
className: "mt-4",
|
|
33
|
+
children
|
|
34
|
+
}),
|
|
35
|
+
/* @__PURE__ */ jsx("div", {
|
|
36
|
+
className: "mt-5 flex flex-col-reverse gap-2 [&>*]:min-h-11",
|
|
37
|
+
children: actions
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ResponsiveDialog };
|