@protobase/ui 0.12.21 → 0.13.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 (51) hide show
  1. package/dist/app/displays/card-layout.js +19 -0
  2. package/dist/app/displays/cards-display.js +17 -143
  3. package/dist/app/displays/display-controls.js +3 -2
  4. package/dist/app/displays/list-cards.js +50 -0
  5. package/dist/app/displays/list-displays.d.ts +1 -1
  6. package/dist/app/displays/list-displays.js +40 -6
  7. package/dist/app/displays/map-display.js +120 -0
  8. package/dist/app/displays/map-groups.js +55 -0
  9. package/dist/app/displays/record-card.js +171 -0
  10. package/dist/app/displays/virtual-cards.js +138 -0
  11. package/dist/app/embedded-list/embedded-columns.js +52 -0
  12. package/dist/app/embedded-list/embedded-display.js +26 -0
  13. package/dist/app/embedded-list/embedded-list.js +155 -0
  14. package/dist/app/embedded-list/use-embedded-display.js +23 -0
  15. package/dist/app/embedded-list/use-embedded-rows.js +33 -0
  16. package/dist/app/field-editor.js +1 -1
  17. package/dist/app/list-page.js +32 -11
  18. package/dist/app/list-scroll.js +25 -0
  19. package/dist/app/map/record-map.js +2 -2
  20. package/dist/app/map/records-map.js +106 -0
  21. package/dist/app/named-icon.js +3 -2
  22. package/dist/app/pages/blocks/table.js +21 -41
  23. package/dist/app/pages/field-value.js +2 -2
  24. package/dist/app/record-page.js +3 -2
  25. package/dist/app/related-section.js +30 -52
  26. package/dist/app/router.js +1 -0
  27. package/dist/data-grid/data-grid.js +19 -39
  28. package/dist/data-grid/geometry.js +7 -9
  29. package/dist/data-grid/use-grid-scroll.js +8 -1
  30. package/dist/data-grid/use-virtual-rows.js +31 -0
  31. package/dist/live/columns-from-model.js +6 -4
  32. package/dist/live/grid-loader.js +26 -10
  33. package/dist/package.js +1 -1
  34. package/dist/record-view/section.js +24 -17
  35. package/dist/shell/assets/app-DLjaMVJD.js +66 -0
  36. package/dist/shell/assets/index-BYPphaEm.css +2 -0
  37. package/dist/shell/assets/{index-BNrginzc.js → index-D7IcSr6n.js} +3 -3
  38. package/dist/shell/assets/maplibre-gl-Bt-3iOM-.js +1 -0
  39. package/dist/shell/assets/record-map-DL4b9dZO.js +814 -0
  40. package/dist/shell/assets/records-map-D0EOO5kZ.js +1 -0
  41. package/dist/shell/assets/shared-protobase-ui-B8UXOIeW.js +1 -0
  42. package/dist/shell/index.html +5 -5
  43. package/dist/shell/protobase.shell.json +1 -1
  44. package/package.json +4 -5
  45. package/dist/app/pages/record-table.js +0 -114
  46. package/dist/app/pages/use-relation-labels.js +0 -30
  47. package/dist/shell/assets/app-CdxyCTXE.js +0 -66
  48. package/dist/shell/assets/index-UCjnDdFX.css +0 -2
  49. package/dist/shell/assets/maplibre-gl-B1f3q0xK.js +0 -1
  50. package/dist/shell/assets/record-map-HYe7IrpM.js +0 -814
  51. package/dist/shell/assets/shared-protobase-ui-DkwITozF.js +0 -1
@@ -0,0 +1,171 @@
1
+ import { Badge } from "../../primitives/badge.js";
2
+ import { humanize } from "../../live/naming.js";
3
+ import { useProjectUi } from "../pages/project-ui.js";
4
+ import { fileShown, isPreviewable } from "../../live/file-values.js";
5
+ import { displayValue } from "../../live/display-value.js";
6
+ import { amountParts, cardFields, filledAmount, filledMeta } from "./list-displays.js";
7
+ import { AppLink } from "../use-app-address.js";
8
+ import { shownWidget } from "../field-types/shown-widget.js";
9
+ import { CustomBoundary } from "../pages/custom-boundary.js";
10
+ import { WidgetPart } from "../field-types/widget-part.js";
11
+ import { namedIcon } from "../named-icon.js";
12
+ import { ValueBadge } from "../value-badge.js";
13
+ import { CardCarousel } from "./card-carousel.js";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ //#region src/app/displays/record-card.tsx
16
+ const CardImage = ({ value }) => {
17
+ const file = fileShown(value);
18
+ const url = file && "url" in file && typeof file.url === "string" && isPreviewable(file.type) ? file.url : void 0;
19
+ return /* @__PURE__ */ jsx("span", {
20
+ className: "-mx-3 -mt-3 mb-1.5 block aspect-[4/3] overflow-hidden rounded-t-lg border-b border-border bg-muted",
21
+ children: url && /* @__PURE__ */ jsx("img", {
22
+ src: url,
23
+ alt: "",
24
+ loading: "lazy",
25
+ className: "size-full object-cover"
26
+ })
27
+ });
28
+ };
29
+ const MetaValue = ({ icon, children }) => {
30
+ const Icon = namedIcon(icon);
31
+ return /* @__PURE__ */ jsxs("dd", {
32
+ className: "flex items-center gap-1 tabular-nums",
33
+ children: [Icon && /* @__PURE__ */ jsx(Icon, {
34
+ className: "size-3.5 shrink-0",
35
+ "aria-hidden": true
36
+ }), children]
37
+ });
38
+ };
39
+ /**
40
+ * A record's card of a cards display, opening its page: its image across the top, with arrows through its photos when
41
+ * the display names them, the title over the subtitle, the badge beside it, the first amount with a value in bold, small
42
+ * meta fields below, those with a value, the labels of its flags that are true and the footer's fields with a value,
43
+ * joined by bullets; or, with a `card` component, what it draws of the row, the built-in content again when it is
44
+ * missing or throws.
45
+ */
46
+ const RecordCard = ({ model, view, display, row }) => {
47
+ const { image, title, subtitle, amount, badge, meta, footer, flags } = cardFields(model, view, display);
48
+ const label = (name) => view?.fields[name]?.label ?? humanize(name);
49
+ const text = (name) => {
50
+ const field = model.fields[name];
51
+ if (!field) return "";
52
+ return row.labels[name] ?? displayValue(field, row.record[name], view?.fields[name]);
53
+ };
54
+ const { fields: fieldTypes, components } = useProjectUi();
55
+ const Card = display.card === void 0 ? void 0 : components?.[display.card];
56
+ const shownValue = (name) => {
57
+ const field = model.fields[name];
58
+ const shown = field && shownWidget(fieldTypes, field, view?.fields[name]);
59
+ return shown ? /* @__PURE__ */ jsx(WidgetPart, {
60
+ shown,
61
+ part: "display",
62
+ field,
63
+ value: row.record[name],
64
+ fallback: text(name)
65
+ }) : text(name);
66
+ };
67
+ const metaValue = (name) => {
68
+ const field = model.fields[name];
69
+ const hints = view?.fields[name];
70
+ const value = row.record[name];
71
+ if (!field || hints?.format !== "badge" || shownWidget(fieldTypes, field, hints) || value === null || value === void 0 || value === "") return shownValue(name);
72
+ return /* @__PURE__ */ jsx(ValueBadge, {
73
+ value,
74
+ hints,
75
+ children: text(name)
76
+ });
77
+ };
78
+ const amountValue = (name) => {
79
+ const field = model.fields[name];
80
+ if (!field) return void 0;
81
+ if (row.labels[name] !== void 0 || shownWidget(fieldTypes, field, view?.fields[name])) return /* @__PURE__ */ jsx("strong", {
82
+ className: "font-semibold text-foreground",
83
+ children: shownValue(name)
84
+ });
85
+ const parts = amountParts(field, row.record[name], view?.fields[name]);
86
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("strong", {
87
+ className: "font-semibold text-foreground",
88
+ children: parts.amount
89
+ }), parts.suffix] });
90
+ };
91
+ const photos = Card ? void 0 : display.photos;
92
+ const status = badge === void 0 ? void 0 : row.record[badge];
93
+ const raised = flags.filter((name) => row.record[name] === true);
94
+ const cover = image === void 0 ? void 0 : row.record[image];
95
+ const shownAmount = filledAmount(row, amount);
96
+ const footing = filledMeta(row, footer);
97
+ const builtIn = (photo) => /* @__PURE__ */ jsxs(Fragment, { children: [
98
+ (image !== void 0 || photos !== void 0) && /* @__PURE__ */ jsx(CardImage, { value: photo }),
99
+ /* @__PURE__ */ jsxs("span", {
100
+ className: "flex items-start justify-between gap-2",
101
+ children: [/* @__PURE__ */ jsx("span", {
102
+ className: "min-w-0 truncate font-medium",
103
+ children: shownValue(title)
104
+ }), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(ValueBadge, {
105
+ value: status,
106
+ hints: view?.fields[badge],
107
+ children: text(badge)
108
+ })]
109
+ }),
110
+ subtitle !== void 0 && /* @__PURE__ */ jsx("span", {
111
+ className: "truncate text-muted-foreground",
112
+ children: shownValue(subtitle)
113
+ }),
114
+ shownAmount !== void 0 && /* @__PURE__ */ jsx("span", {
115
+ className: "truncate tabular-nums text-muted-foreground",
116
+ children: amountValue(shownAmount)
117
+ }),
118
+ meta.length > 0 && /* @__PURE__ */ jsx("dl", {
119
+ className: "mt-auto flex flex-wrap items-center gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
120
+ children: filledMeta(row, meta).map((name) => /* @__PURE__ */ jsxs("div", {
121
+ className: "flex gap-1",
122
+ children: [/* @__PURE__ */ jsx("dt", {
123
+ className: "sr-only",
124
+ children: label(name)
125
+ }), /* @__PURE__ */ jsx(MetaValue, {
126
+ icon: view?.fields[name]?.icon,
127
+ children: metaValue(name)
128
+ })]
129
+ }, name))
130
+ }),
131
+ raised.length > 0 && /* @__PURE__ */ jsx("ul", {
132
+ className: "flex flex-wrap gap-1",
133
+ "aria-label": "Features",
134
+ children: raised.map((name) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Badge, {
135
+ dot: false,
136
+ children: label(name)
137
+ }) }, name))
138
+ }),
139
+ footing.length > 0 && /* @__PURE__ */ jsx("span", {
140
+ className: `truncate text-xs text-muted-foreground ${meta.length > 0 ? "" : "mt-auto"}`,
141
+ children: footing.map((name, index) => /* @__PURE__ */ jsxs("span", { children: [index > 0 && /* @__PURE__ */ jsx("span", {
142
+ "aria-hidden": true,
143
+ children: " • "
144
+ }), shownValue(name)] }, name))
145
+ })
146
+ ] });
147
+ const card = (photo) => /* @__PURE__ */ jsx(AppLink, {
148
+ to: {
149
+ kind: "record",
150
+ resource: model.name,
151
+ key: row.key
152
+ },
153
+ className: "flex h-full flex-col gap-1.5 rounded-lg border border-border bg-background p-3 text-[13px] hover:border-border-strong hover:bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
154
+ children: Card ? /* @__PURE__ */ jsx(CustomBoundary, {
155
+ name: display.card,
156
+ fallback: builtIn(photo),
157
+ children: /* @__PURE__ */ jsx(Card, { row })
158
+ }) : builtIn(photo)
159
+ });
160
+ return /* @__PURE__ */ jsx("div", {
161
+ className: photos ? "group/card relative h-full" : "h-full",
162
+ children: photos ? /* @__PURE__ */ jsx(CardCarousel, {
163
+ photos,
164
+ recordKey: row.key,
165
+ cover,
166
+ children: card
167
+ }) : card(cover)
168
+ });
169
+ };
170
+ //#endregion
171
+ export { RecordCard };
@@ -0,0 +1,138 @@
1
+ import { Skeleton } from "../../primitives/skeleton.js";
2
+ import { usePagedRows } from "../../data-grid/use-paged-rows.js";
3
+ import { useVirtualRows } from "../../data-grid/use-virtual-rows.js";
4
+ import { anchoredTop, cardColumns, cardWidth, recordRange } from "./card-layout.js";
5
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/app/displays/virtual-cards.tsx
8
+ /**
9
+ * Cards in rows as wide as the list, scrolled like the table: only the rows in view are drawn, over a scroller that
10
+ * holds any number of records, and the records are loaded in the table's pages, so a change of width keeps the loaded
11
+ * pages and the first record in view. Every card in a width is as tall as the tallest drawn so far.
12
+ */
13
+ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, children }) => {
14
+ const { total } = source;
15
+ const [width, setWidth] = useState(0);
16
+ const columns = cardColumns(width);
17
+ const itemWidth = cardWidth(width, columns);
18
+ const [measured, setMeasured] = useState({
19
+ width: 0,
20
+ height: 0
21
+ });
22
+ const itemHeight = Math.ceil(Math.max(minHeight(itemWidth), measured.width === itemWidth ? measured.height : 0));
23
+ const rowHeight = itemHeight + 12;
24
+ const layout = {
25
+ columns,
26
+ rowHeight
27
+ };
28
+ const anchor = useRef({
29
+ layout,
30
+ top: 0
31
+ });
32
+ const changed = anchor.current.layout.columns !== columns || anchor.current.layout.rowHeight !== rowHeight;
33
+ const settle = (top) => changed ? anchoredTop(anchor.current.top, anchor.current.layout, layout) : top;
34
+ const scroll = useVirtualRows({
35
+ total: Math.ceil(total / columns),
36
+ rowHeight,
37
+ initialIndex: Math.floor(initialIndex / columns),
38
+ settle
39
+ });
40
+ useLayoutEffect(() => {
41
+ if (changed) scroll.scrollTo(scroll.top);
42
+ anchor.current = {
43
+ layout,
44
+ top: scroll.top
45
+ };
46
+ if (!changed) onScrollIndex(Math.floor(scroll.top / rowHeight) * columns);
47
+ });
48
+ const range = recordRange(scroll.first, scroll.last, columns, total);
49
+ const getRow = usePagedRows({
50
+ total,
51
+ rowAt: source.rowAt,
52
+ loadRows: source.loadRows,
53
+ latencyMs: 0
54
+ }, range.start, range.end);
55
+ const bodyRef = useRef(null);
56
+ useLayoutEffect(() => {
57
+ const body = bodyRef.current;
58
+ if (!body) return;
59
+ const observer = new ResizeObserver(() => setWidth(body.clientWidth));
60
+ observer.observe(body);
61
+ setWidth(body.clientWidth);
62
+ return () => observer.disconnect();
63
+ }, []);
64
+ const widthRef = useRef(itemWidth);
65
+ widthRef.current = itemWidth;
66
+ const [observer] = useState(() => new ResizeObserver((entries) => {
67
+ const tallest = Math.max(...entries.map((entry) => entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height));
68
+ const at = widthRef.current;
69
+ setMeasured((current) => current.width === at && current.height >= tallest ? current : {
70
+ width: at,
71
+ height: Math.max(tallest, current.width === at ? current.height : 0)
72
+ });
73
+ }));
74
+ useLayoutEffect(() => () => observer.disconnect(), [observer]);
75
+ const observe = useCallback((item) => {
76
+ if (!item) return;
77
+ observer.observe(item);
78
+ return () => observer.unobserve(item);
79
+ }, [observer]);
80
+ const firstShown = Math.min(total, Math.floor(scroll.top / rowHeight) * columns + 1);
81
+ const lastShown = Math.min(total, (Math.floor((scroll.top + Math.max(0, scroll.viewport - 1)) / rowHeight) + 1) * columns);
82
+ return /* @__PURE__ */ jsxs("div", {
83
+ className: "flex min-h-0 flex-1 flex-col",
84
+ children: [/* @__PURE__ */ jsx("div", {
85
+ ref: scroll.ref,
86
+ onScroll: scroll.onScroll,
87
+ className: "relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
88
+ children: /* @__PURE__ */ jsx("div", {
89
+ style: { height: scroll.geometry.contentHeight || void 0 },
90
+ children: /* @__PURE__ */ jsx("div", {
91
+ ref: bodyRef,
92
+ className: "sticky top-0 overflow-clip",
93
+ style: { height: scroll.viewport },
94
+ children: /* @__PURE__ */ jsx("ul", {
95
+ className: "relative",
96
+ "aria-label": label,
97
+ children: scroll.rows.flatMap(({ index: rowIndex, offset }) => Array.from({ length: columns }, (_, column) => {
98
+ const index = rowIndex * columns + column;
99
+ if (index >= total) return [];
100
+ const row = getRow(index);
101
+ const place = {
102
+ top: offset,
103
+ left: column * (itemWidth + 12),
104
+ width: itemWidth,
105
+ minHeight: itemHeight
106
+ };
107
+ if (!row) return /* @__PURE__ */ jsx("li", {
108
+ "aria-hidden": true,
109
+ className: "absolute grid grid-cols-[minmax(0,1fr)]",
110
+ style: place,
111
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "h-full rounded-lg" })
112
+ }, `skeleton-${index}`);
113
+ return /* @__PURE__ */ jsx("li", {
114
+ ref: observe,
115
+ "aria-posinset": index + 1,
116
+ "aria-setsize": total,
117
+ className: "absolute grid grid-cols-[minmax(0,1fr)]",
118
+ style: place,
119
+ children: children(row)
120
+ }, row.id);
121
+ }))
122
+ })
123
+ })
124
+ })
125
+ }), /* @__PURE__ */ jsxs("div", {
126
+ className: "py-3 text-xs tabular-nums text-muted-foreground",
127
+ children: [
128
+ firstShown.toLocaleString("en-IE"),
129
+ "–",
130
+ lastShown.toLocaleString("en-IE"),
131
+ " of ",
132
+ total.toLocaleString("en-IE")
133
+ ]
134
+ })]
135
+ });
136
+ };
137
+ //#endregion
138
+ export { VirtualCards };
@@ -0,0 +1,52 @@
1
+ import { humanize } from "../../live/naming.js";
2
+ import { displayRow } from "../displays/list-displays.js";
3
+ import { columnsFromModel } from "../../live/columns-from-model.js";
4
+ import { withFieldTypeCells } from "../field-types/field-type-cells.js";
5
+ import { scopeOf } from "../pages/use-layout-data.js";
6
+ import { FieldValue } from "../pages/field-value.js";
7
+ import { usePathValue } from "../pages/use-path-value.js";
8
+ import { jsx } from "react/jsx-runtime";
9
+ import { resolveFieldPath } from "@protobase/layout";
10
+ //#region src/app/embedded-list/embedded-columns.tsx
11
+ const isPath = (column) => column.includes(".");
12
+ /** A value through relations (`unit_id.property_id`), fetched like the record page fetches it and cached. */
13
+ const PathCell = ({ model, row, column }) => {
14
+ const value = usePathValue(scopeOf(model, displayRow(model, row).record), column);
15
+ if (value.state === "loading") return /* @__PURE__ */ jsx("span", {
16
+ className: "text-muted-foreground",
17
+ children: "…"
18
+ });
19
+ if (value.state === "missing") return /* @__PURE__ */ jsx("span", {
20
+ className: "text-muted-foreground",
21
+ children: "—"
22
+ });
23
+ return /* @__PURE__ */ jsx(FieldValue, {
24
+ model: value.model,
25
+ field: value.field,
26
+ value: value.value
27
+ });
28
+ };
29
+ /** A column through relations, headed by the label of the field it ends at. */
30
+ const pathColumn = (model, column, { resources, views }) => {
31
+ const resolved = resolveFieldPath(resources, model.name, column);
32
+ const last = resolved.ok ? resolved.steps.at(-1) : void 0;
33
+ const name = last?.field.name ?? column;
34
+ return {
35
+ id: column,
36
+ header: views[last?.model.name ?? model.name]?.fields[name]?.label ?? humanize(name),
37
+ kind: "text",
38
+ width: 200,
39
+ render: (row) => /* @__PURE__ */ jsx(PathCell, {
40
+ model,
41
+ row,
42
+ column
43
+ })
44
+ };
45
+ };
46
+ /** The list page's columns for `names`: fields of `model` as its table draws them, and paths through relations. */
47
+ const embeddedColumns = (model, view, names, meta) => {
48
+ const fields = new Map(withFieldTypeCells(columnsFromModel(model, view, [], names.filter((name) => !isPath(name))), model, view, meta.fieldTypes).map((spec) => [spec.id, spec]));
49
+ return names.flatMap((name) => isPath(name) ? [pathColumn(model, name, meta)] : fields.get(name) ?? []);
50
+ };
51
+ //#endregion
52
+ export { embeddedColumns };
@@ -0,0 +1,26 @@
1
+ import { guarded } from "../../lib/guarded.js";
2
+ import { defaultDisplay, displayChoices } from "../displays/list-displays.js";
3
+ import { changedList, listDefaults } from "../current-address.js";
4
+ //#region src/app/embedded-list/embedded-display.ts
5
+ const prefix = "protobase:embedded-display:";
6
+ /** Where a viewer's pick of display is kept for one embedded list, named by the place it is shown. */
7
+ const embeddedDisplayKey = (place) => `${prefix}${place}`;
8
+ const readPickedDisplay = (storage, key) => guarded(() => storage.getItem(key) ?? void 0, void 0);
9
+ const writePickedDisplay = (storage, key, display) => guarded(() => storage.setItem(key, display), void 0);
10
+ /**
11
+ * The display an embedded list shows: the viewer's pick while the view still has it, else what the list opens in for
12
+ * the screen, with the display the page names in place of the view's default (below `sm`, a cards display when the
13
+ * view has one).
14
+ */
15
+ const embeddedDisplay = (view, xs, { picked, preferred }) => {
16
+ const names = displayChoices(view).map((choice) => choice.name);
17
+ if (picked !== void 0 && names.includes(picked)) return picked;
18
+ return defaultDisplay(view, xs, preferred !== void 0 && names.includes(preferred) ? preferred : void 0);
19
+ };
20
+ /** The resource's list with the embedded list's filter, order and display, leaving out what the list does by default. */
21
+ const viewAllAddress = (model, view, xs, list) => changedList({
22
+ kind: "list",
23
+ resource: model.name
24
+ }, list, listDefaults(model, view, xs));
25
+ //#endregion
26
+ export { embeddedDisplay, embeddedDisplayKey, readPickedDisplay, viewAllAddress, writePickedDisplay };
@@ -0,0 +1,155 @@
1
+ import { useIsDesktop, useIsXs } from "../../lib/use-media-query.js";
2
+ import { Skeleton } from "../../primitives/skeleton.js";
3
+ import { recordKey } from "../../live/model-helpers.js";
4
+ import { humanize } from "../../live/naming.js";
5
+ import { useProjectUi } from "../pages/project-ui.js";
6
+ import { displayRow, loadedFields, shownDisplay } from "../displays/list-displays.js";
7
+ import { defaultOrderBy, parseSort, sortToOrderBy } from "../list-order.js";
8
+ import { useAdminMeta } from "../meta-gate.js";
9
+ import { AppLink, useAppAddress } from "../use-app-address.js";
10
+ import { ErrorBanner } from "../error-banner.js";
11
+ import { DataGrid } from "../../data-grid/data-grid.js";
12
+ import { listColumnNames } from "../../live/columns-from-model.js";
13
+ import { MapDisplay } from "../displays/map-display.js";
14
+ import { CustomDisplay } from "../displays/custom-display.js";
15
+ import { DisplaySwitch } from "../displays/display-controls.js";
16
+ import { CardsDisplay } from "../displays/cards-display.js";
17
+ import { embeddedColumns } from "./embedded-columns.js";
18
+ import { viewAllAddress } from "./embedded-display.js";
19
+ import { useEmbeddedRows } from "./use-embedded-rows.js";
20
+ import { useMemo, useState } from "react";
21
+ import { ArrowRight } from "lucide-react";
22
+ import { jsx, jsxs } from "react/jsx-runtime";
23
+ //#region src/app/embedded-list/embedded-list.tsx
24
+ /** The switch between the table and the view's displays, for an embedded list's heading; nothing with only a table. */
25
+ const EmbeddedDisplaySwitch = ({ view, value, onChange }) => /* @__PURE__ */ jsx(DisplaySwitch, {
26
+ view,
27
+ value,
28
+ onChange,
29
+ menu: !useIsDesktop()
30
+ });
31
+ const Loading = () => /* @__PURE__ */ jsx("div", {
32
+ className: "flex flex-col gap-2",
33
+ children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, index))
34
+ });
35
+ /**
36
+ * The first `limit` records of a list, drawn by the list page's own table (sized to its rows) or display, and a link
37
+ * to the whole list with the same filter, order and display. On a phone a cards display is one row that scrolls sideways.
38
+ */
39
+ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty }) => {
40
+ const { resources, views } = useAdminMeta();
41
+ const { fields: fieldTypes } = useProjectUi();
42
+ const { go } = useAppAddress();
43
+ const xs = useIsXs();
44
+ const [orderBy, setOrderBy] = useState(sort || defaultOrderBy(model, view));
45
+ const names = columns ?? listColumnNames(model, view);
46
+ const specs = useMemo(() => embeddedColumns(model, view, names, {
47
+ resources,
48
+ views,
49
+ fieldTypes
50
+ }), [
51
+ model,
52
+ view,
53
+ names.join(","),
54
+ resources,
55
+ views,
56
+ fieldTypes
57
+ ]);
58
+ const fields = useMemo(() => loadedFields(model, view, specs.map((spec) => spec.id)), [
59
+ model,
60
+ view,
61
+ specs
62
+ ]);
63
+ const rows = useEmbeddedRows({
64
+ model,
65
+ filter,
66
+ orderBy,
67
+ limit,
68
+ fields
69
+ });
70
+ const loaded = rows.data;
71
+ const source = useMemo(() => loaded && {
72
+ total: loaded.length,
73
+ rowAt: (index) => loaded[index]
74
+ }, [loaded]);
75
+ const shown = shownDisplay(view, display);
76
+ if (rows.isPending) return /* @__PURE__ */ jsx(Loading, {});
77
+ if (rows.error) return /* @__PURE__ */ jsx(ErrorBanner, {
78
+ title: "Could not load",
79
+ error: rows.error
80
+ });
81
+ if (!source || source.total === 0) return /* @__PURE__ */ jsx("p", {
82
+ className: "text-[13px] text-muted-foreground",
83
+ children: empty ?? "Nothing here yet."
84
+ });
85
+ const list = {
86
+ resource: model.name,
87
+ rows: rows.data.map((row) => displayRow(model, row)),
88
+ total: rows.data.length,
89
+ loading: false,
90
+ error: null,
91
+ more: false,
92
+ loadMore: () => void 0
93
+ };
94
+ const openRecord = (row) => go({
95
+ kind: "record",
96
+ resource: model.name,
97
+ key: recordKey(model, row)
98
+ });
99
+ const singular = view?.names?.singular ?? humanize(model.name);
100
+ const plural = view?.names?.plural ?? humanize(model.name);
101
+ return /* @__PURE__ */ jsxs("div", {
102
+ className: "flex flex-col gap-3",
103
+ children: [!shown ? /* @__PURE__ */ jsx(DataGrid, {
104
+ fit: true,
105
+ columns: specs,
106
+ source,
107
+ latencyMs: 0,
108
+ sort: parseSort(orderBy),
109
+ onSortChange: (next) => setOrderBy(sortToOrderBy(next)),
110
+ onOpenRow: openRecord,
111
+ rowActions: [{
112
+ label: `Open ${singular.toLowerCase()}`,
113
+ onSelect: openRecord
114
+ }],
115
+ rowNumbers: false,
116
+ selectable: false
117
+ }) : shown.kind === "cards" ? /* @__PURE__ */ jsx(CardsDisplay, {
118
+ model,
119
+ view,
120
+ display: shown,
121
+ list,
122
+ row: xs,
123
+ footer: false
124
+ }) : shown.kind === "map" ? /* @__PURE__ */ jsx(MapDisplay, {
125
+ model,
126
+ view,
127
+ display: shown,
128
+ list,
129
+ footer: false
130
+ }) : /* @__PURE__ */ jsx(CustomDisplay, {
131
+ display: shown,
132
+ list
133
+ }), /* @__PURE__ */ jsxs(AppLink, {
134
+ to: viewAllAddress(model, view, xs, {
135
+ filter,
136
+ orderBy,
137
+ display
138
+ }),
139
+ className: "flex items-center gap-1 self-end text-[13px] font-medium text-primary-text hover:underline",
140
+ children: [
141
+ "View all ",
142
+ /* @__PURE__ */ jsx("span", {
143
+ className: "sr-only",
144
+ children: plural.toLowerCase()
145
+ }),
146
+ /* @__PURE__ */ jsx(ArrowRight, {
147
+ className: "size-3.5",
148
+ "aria-hidden": true
149
+ })
150
+ ]
151
+ })]
152
+ });
153
+ };
154
+ //#endregion
155
+ export { EmbeddedDisplaySwitch, EmbeddedList };
@@ -0,0 +1,23 @@
1
+ import { useIsXs } from "../../lib/use-media-query.js";
2
+ import { embeddedDisplay, embeddedDisplayKey, readPickedDisplay, writePickedDisplay } from "./embedded-display.js";
3
+ import { useCallback, useState } from "react";
4
+ //#region src/app/embedded-list/use-embedded-display.ts
5
+ /**
6
+ * The display an embedded list shows and the way to pick another, which is kept in local storage for the viewer under
7
+ * `place`, the list's place in the app.
8
+ */
9
+ const useEmbeddedDisplay = (view, place, preferred) => {
10
+ const xs = useIsXs();
11
+ const key = embeddedDisplayKey(place);
12
+ const [picked, setPicked] = useState(() => readPickedDisplay(localStorage, key));
13
+ const pick = useCallback((display) => {
14
+ setPicked(display);
15
+ writePickedDisplay(localStorage, key, display);
16
+ }, [key]);
17
+ return [embeddedDisplay(view, xs, {
18
+ picked,
19
+ preferred
20
+ }), pick];
21
+ };
22
+ //#endregion
23
+ export { useEmbeddedDisplay };
@@ -0,0 +1,33 @@
1
+ import { keys } from "../../data/query-keys.js";
2
+ import { useClient } from "../../data/api-provider.js";
3
+ import { recordId } from "../../live/model-helpers.js";
4
+ import { withLabels } from "../../live/relation-labels.js";
5
+ import { useAdminMeta } from "../meta-gate.js";
6
+ import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
7
+ //#region src/app/embedded-list/use-embedded-rows.ts
8
+ /**
9
+ * The first `limit` records in `orderBy` order, as the list page's table and displays get them: relations among
10
+ * `fields` as `{ id, name }`. A filter through a join resource may hold hundreds of keys, so it goes in a JSON body.
11
+ */
12
+ const useEmbeddedRows = ({ model, filter, orderBy, limit, fields }) => {
13
+ const client = useClient();
14
+ const queryClient = useQueryClient();
15
+ const { resources, views } = useAdminMeta();
16
+ return useQuery({
17
+ queryKey: [...keys.list(model.name, filter, orderBy, limit, "embedded"), fields.join(",")],
18
+ placeholderData: keepPreviousData,
19
+ queryFn: async () => {
20
+ const page = await client.search(model.name, {
21
+ filter: filter || void 0,
22
+ orderBy: orderBy || void 0,
23
+ pageSize: limit
24
+ });
25
+ return (await withLabels(queryClient, client, resources, views, model, fields, page.items)).map((row, index) => ({
26
+ ...row,
27
+ id: recordId(model, page.items[index])
28
+ }));
29
+ }
30
+ });
31
+ };
32
+ //#endregion
33
+ export { useEmbeddedRows };
@@ -1,9 +1,9 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Input } from "../primitives/input.js";
3
3
  import { Switch } from "../primitives/switch.js";
4
+ import { displayValue } from "../live/display-value.js";
4
5
  import { AppLink } from "./use-app-address.js";
5
6
  import { RelationPicker } from "./relation-picker.js";
6
- import { displayValue } from "../live/display-value.js";
7
7
  import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
8
8
  import { SensitiveField } from "./sensitive-field.js";
9
9
  import { FileField } from "./file-field.js";