@protobase/ui 0.14.1 → 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-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/embedded-list/embedded-columns.js +1 -1
- package/dist/app/embedded-list/use-embedded-rows.js +11 -6
- package/dist/app/list-page.js +44 -11
- 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/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/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-BCLK6g9a.js → index-DTbA97zz.js} +1 -1
- package/dist/shell/assets/{maplibre-gl-DWx1yzYO.js → maplibre-gl-CSTJJJQT.js} +1 -1
- package/dist/shell/assets/{record-map-C2M8dEjy.js → record-map-CeTF-ArN.js} +2 -2
- package/dist/shell/assets/{records-map-huS0J9xV.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/shell/assets/app-Dkz6z_t_.js +0 -66
- package/dist/shell/assets/index-BQ4FIU4i.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-CsJxPCx6.js +0 -1
|
@@ -2,6 +2,24 @@ import { defaultDisplay } from "./displays/list-displays.js";
|
|
|
2
2
|
import { defaultOrderBy } from "./list-order.js";
|
|
3
3
|
import { encodeKey } from "@protobase/schema/internal";
|
|
4
4
|
//#region src/app/current-address.ts
|
|
5
|
+
/**
|
|
6
|
+
* The resource whose records a list shows, with its view: the list's own, or, when its view has `listOf`, the resource
|
|
7
|
+
* that relation points at, if the caller can see it.
|
|
8
|
+
*/
|
|
9
|
+
const listedResource = (resources, views, resource) => {
|
|
10
|
+
const model = resources[resource];
|
|
11
|
+
const listOf = views[resource]?.listOf;
|
|
12
|
+
const target = listOf === void 0 ? void 0 : model?.fields[listOf]?.relation?.resource;
|
|
13
|
+
return target !== void 0 && resources[target] ? {
|
|
14
|
+
model: resources[target],
|
|
15
|
+
view: views[target],
|
|
16
|
+
listOf: true
|
|
17
|
+
} : {
|
|
18
|
+
model,
|
|
19
|
+
view: views[resource],
|
|
20
|
+
listOf: false
|
|
21
|
+
};
|
|
22
|
+
};
|
|
5
23
|
/** What a list shows when its address names nothing: its default display for the screen, the list's sort, the filters in a bar. */
|
|
6
24
|
const listDefaults = (model, view, xs = false) => ({
|
|
7
25
|
display: defaultDisplay(view, xs),
|
|
@@ -40,4 +58,4 @@ const changedList = (read, change, defaults) => {
|
|
|
40
58
|
};
|
|
41
59
|
};
|
|
42
60
|
//#endregion
|
|
43
|
-
export { changedList, currentAddress, listDefaults };
|
|
61
|
+
export { changedList, currentAddress, listDefaults, listedResource };
|
|
@@ -1,46 +1,58 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { cardPhotoList, cardPhotosRequest, shownPhoto } from "./card-photos.js";
|
|
6
|
-
import { useState } from "react";
|
|
2
|
+
import { CardImageFrame, CardPhoto } from "./card-image.js";
|
|
3
|
+
import { cardPhotoList, scrolledPhoto, shownPhoto } from "./card-photos.js";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
7
5
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
8
6
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { useQuery } from "@tanstack/react-query";
|
|
10
7
|
//#region src/app/displays/card-carousel.tsx
|
|
11
|
-
const arrow = "pointer-events-auto absolute top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-full bg-background/85 text-foreground shadow-sm opacity-0 transition-opacity group-hover/card:opacity-100 group-focus-
|
|
8
|
+
const arrow = "pointer-events-auto absolute top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-full bg-background/85 text-foreground shadow-sm opacity-0 transition-opacity group-has-[[data-card-photos]:hover]/card:opacity-100 group-hover/arrows:opacity-100 group-has-[:focus-visible]/arrows:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30";
|
|
9
|
+
const touchArrow = "[@media(hover:none)]:opacity-100";
|
|
12
10
|
/**
|
|
13
|
-
* A card whose picture steps through its photos
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* A card whose picture steps through its photos, the card's own image first: a track that snaps photo by photo, swiped
|
|
12
|
+
* on a touch screen or moved by arrows, with dots marking the one in view. The photos come with the card's row, in the
|
|
13
|
+
* same answer as the list, so the dots mark its own image from the start. The track sits inside the card's link, so a
|
|
14
|
+
* tap opens the record and a swipe does not; the arrows sit over it, beside the link in the document, so pressing one
|
|
15
|
+
* does not open the record either. The card's list item must be `relative group/card`.
|
|
16
16
|
*/
|
|
17
|
-
const CardCarousel = ({ photos,
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const [
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
enabled: opened,
|
|
27
|
-
queryFn: async () => (await client.list(photos.resource, request)).items.map((item) => item[photos.image])
|
|
28
|
-
});
|
|
29
|
-
const list = cardPhotoList(cover, loaded.data ?? []);
|
|
30
|
-
const index = shownPhoto(step, list.length);
|
|
17
|
+
const CardCarousel = ({ photos, cover, swipe = true, children }) => {
|
|
18
|
+
const track = useRef(null);
|
|
19
|
+
const aim = useRef(void 0);
|
|
20
|
+
const [index, setIndex] = useState(0);
|
|
21
|
+
const list = cardPhotoList(cover, photos);
|
|
22
|
+
const show = (position) => {
|
|
23
|
+
const scroller = track.current;
|
|
24
|
+
if (scroller) scroller.scrollTo({ left: position * scroller.clientWidth });
|
|
25
|
+
};
|
|
31
26
|
const press = (by) => {
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
aim.current = shownPhoto((aim.current ?? index) + by, list.length);
|
|
28
|
+
show(aim.current);
|
|
29
|
+
};
|
|
30
|
+
const touch = () => {
|
|
31
|
+
aim.current = void 0;
|
|
34
32
|
};
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const scrolled = (scroller) => {
|
|
34
|
+
setIndex(scrolledPhoto(scroller.scrollLeft, scroller.clientWidth, list.length));
|
|
35
|
+
if (aim.current !== void 0 && Math.abs(scroller.scrollLeft - aim.current * scroller.clientWidth) < 1) aim.current = void 0;
|
|
36
|
+
};
|
|
37
|
+
const picture = /* @__PURE__ */ jsx(CardImageFrame, { children: /* @__PURE__ */ jsx("span", {
|
|
38
|
+
ref: track,
|
|
39
|
+
tabIndex: -1,
|
|
40
|
+
"data-card-photos": true,
|
|
41
|
+
onPointerDown: touch,
|
|
42
|
+
onScroll: (event) => scrolled(event.currentTarget),
|
|
43
|
+
className: cn("flex size-full snap-x snap-mandatory outline-none [scrollbar-width:none] motion-safe:scroll-smooth [&::-webkit-scrollbar]:hidden", swipe ? "overflow-x-auto overscroll-x-contain" : "overflow-x-hidden"),
|
|
44
|
+
children: list.map((photo, position) => /* @__PURE__ */ jsx("span", {
|
|
45
|
+
className: "block size-full shrink-0 snap-start snap-always",
|
|
46
|
+
children: /* @__PURE__ */ jsx(CardPhoto, { value: photo })
|
|
47
|
+
}, position))
|
|
48
|
+
}) });
|
|
49
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(picture), list.length > 1 && /* @__PURE__ */ jsxs("div", {
|
|
50
|
+
className: "group/arrows pointer-events-none absolute inset-x-px top-px aspect-[4/3]",
|
|
39
51
|
children: [
|
|
40
52
|
/* @__PURE__ */ jsx("button", {
|
|
41
53
|
type: "button",
|
|
42
54
|
"aria-label": "Previous photo",
|
|
43
|
-
className: cn(arrow, "left-2"),
|
|
55
|
+
className: cn(arrow, !swipe && touchArrow, "left-2"),
|
|
44
56
|
onClick: () => press(-1),
|
|
45
57
|
children: /* @__PURE__ */ jsx(ChevronLeft, {
|
|
46
58
|
className: "size-4",
|
|
@@ -50,27 +62,23 @@ const CardCarousel = ({ photos, recordKey, cover, children }) => {
|
|
|
50
62
|
/* @__PURE__ */ jsx("button", {
|
|
51
63
|
type: "button",
|
|
52
64
|
"aria-label": "Next photo",
|
|
53
|
-
className: cn(arrow, "right-2"),
|
|
65
|
+
className: cn(arrow, !swipe && touchArrow, "right-2"),
|
|
54
66
|
onClick: () => press(1),
|
|
55
67
|
children: /* @__PURE__ */ jsx(ChevronRight, {
|
|
56
68
|
className: "size-4",
|
|
57
69
|
"aria-hidden": true
|
|
58
70
|
})
|
|
59
71
|
}),
|
|
60
|
-
|
|
61
|
-
className: "absolute inset-x-2 top-2 rounded bg-background/85 px-1.5 py-0.5 text-center text-xs text-danger-text",
|
|
62
|
-
children: "Could not load the photos"
|
|
63
|
-
}),
|
|
64
|
-
loaded.data !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
72
|
+
/* @__PURE__ */ jsxs("span", {
|
|
65
73
|
className: "absolute inset-x-0 bottom-2 flex justify-center",
|
|
66
74
|
"aria-live": "polite",
|
|
67
75
|
children: [/* @__PURE__ */ jsx("span", {
|
|
68
76
|
className: "sr-only",
|
|
69
77
|
children: `Photo ${index + 1} of ${list.length}`
|
|
70
78
|
}), /* @__PURE__ */ jsx("span", {
|
|
71
|
-
className: "flex items-center gap-1 rounded-full bg-background/70 px-1.5 py-1
|
|
79
|
+
className: "flex items-center gap-1 rounded-full bg-background/70 px-1.5 py-1 leading-none",
|
|
72
80
|
"aria-hidden": true,
|
|
73
|
-
children: list.
|
|
81
|
+
children: list.map((_, dot) => /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dot === index ? "bg-foreground" : "bg-foreground/35") }, dot))
|
|
74
82
|
})]
|
|
75
83
|
})
|
|
76
84
|
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { shownImage } from "../../live/file-values.js";
|
|
2
|
+
import { FileImage } from "../../live/file-image.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app/displays/card-image.tsx
|
|
5
|
+
/** A raster image filling its frame; nothing for any other value. */
|
|
6
|
+
const CardPhoto = ({ value }) => {
|
|
7
|
+
const image = shownImage(value, {
|
|
8
|
+
width: 320,
|
|
9
|
+
height: 240
|
|
10
|
+
});
|
|
11
|
+
return image ? /* @__PURE__ */ jsx(FileImage, {
|
|
12
|
+
image,
|
|
13
|
+
loading: "lazy",
|
|
14
|
+
className: "size-full object-cover"
|
|
15
|
+
}) : null;
|
|
16
|
+
};
|
|
17
|
+
/** The frame across a card's top, from the edge of the card in, holding its picture. */
|
|
18
|
+
const CardImageFrame = ({ children }) => /* @__PURE__ */ jsx("span", {
|
|
19
|
+
className: "-mx-3 -mt-3 mb-1.5 block aspect-[4/3] overflow-hidden rounded-t-lg border-b border-border bg-muted",
|
|
20
|
+
children
|
|
21
|
+
});
|
|
22
|
+
/** A picture across the card's top: a raster image only, an empty frame otherwise. */
|
|
23
|
+
const CardImage = ({ value }) => /* @__PURE__ */ jsx(CardImageFrame, { children: /* @__PURE__ */ jsx(CardPhoto, { value }) });
|
|
24
|
+
//#endregion
|
|
25
|
+
export { CardImage, CardImageFrame, CardPhoto };
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import { isStoredFile } from "../../live/file-values.js";
|
|
2
|
-
import {
|
|
2
|
+
import { cardPhotoLimit } from "@protobase/schema/internal";
|
|
3
3
|
//#region src/app/displays/card-photos.ts
|
|
4
|
-
|
|
4
|
+
/** The most photos a card steps through, its own image among them, each marked by a dot. */
|
|
5
|
+
const maxCardDots = cardPhotoLimit;
|
|
5
6
|
/**
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
7
|
+
* The files a card steps through: its own image first, then its photos less that same file, at most `maxCardDots`;
|
|
8
|
+
* empty values are left out.
|
|
8
9
|
*/
|
|
9
|
-
const
|
|
10
|
-
filter: printFilter(where.eq(photos.field, recordKey)),
|
|
11
|
-
...photos.sort !== void 0 && { orderBy: photos.sort },
|
|
12
|
-
fields: placeholder === void 0 ? [photos.image] : [photos.image, placeholder],
|
|
13
|
-
pageSize: 100
|
|
14
|
-
});
|
|
15
|
-
/** The files a card steps through: its own image first, then the loaded photos less that same file; empty values are left out. */
|
|
16
|
-
const cardPhotoList = (cover, loaded) => {
|
|
10
|
+
const cardPhotoList = (cover, photos) => {
|
|
17
11
|
const coverUri = isStoredFile(cover) ? cover.uri : void 0;
|
|
18
|
-
const rest =
|
|
19
|
-
return coverUri === void 0 ? rest : [cover, ...rest];
|
|
12
|
+
const rest = photos.filter((photo) => isStoredFile(photo) && photo.uri !== coverUri);
|
|
13
|
+
return (coverUri === void 0 ? rest : [cover, ...rest]).slice(0, maxCardDots);
|
|
20
14
|
};
|
|
21
15
|
/** The position shown after `step` presses forward, or back when negative, wrapping around both ends. */
|
|
22
16
|
const shownPhoto = (step, count) => count === 0 ? 0 : (step % count + count) % count;
|
|
17
|
+
/** The position a track of `count` photos, each `width` wide, shows when scrolled to `left`: the one mostly in view. */
|
|
18
|
+
const scrolledPhoto = (left, width, count) => width <= 0 || count === 0 ? 0 : Math.min(count - 1, Math.max(0, Math.round(left / width)));
|
|
23
19
|
//#endregion
|
|
24
|
-
export { cardPhotoList,
|
|
20
|
+
export { cardPhotoList, maxCardDots, scrolledPhoto, shownPhoto };
|
|
@@ -7,6 +7,8 @@ export type ListDisplayRow = {
|
|
|
7
7
|
record: Record<string, unknown>;
|
|
8
8
|
/** The name of the record a relation field points at, for the relations the list shows. */
|
|
9
9
|
labels: Record<string, string>;
|
|
10
|
+
/** The files of its first photos, when the list loaded the photos its cards display names. */
|
|
11
|
+
photos?: unknown[];
|
|
10
12
|
};
|
|
11
13
|
/** What a card component of a cards display (`d.cards(..., { card })`) gets: the record it draws, of the display's fields. */
|
|
12
14
|
export type RecordCardProps = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
3
|
import { displayValue } from "../../live/display-value.js";
|
|
4
|
+
import { photosKey } from "../../live/row-photos.js";
|
|
4
5
|
import { encodeKey, tableDisplay } from "@protobase/schema/internal";
|
|
5
6
|
//#region src/app/displays/list-displays.ts
|
|
6
7
|
const displaysOf = (view) => view?.list?.displays ?? [];
|
|
@@ -91,12 +92,16 @@ const loadedFields = (model, view, columns) => [.../* @__PURE__ */ new Set([...c
|
|
|
91
92
|
/** The sortable fields a display shows, for the sort menu that stands in for column headers. */
|
|
92
93
|
const sortFields = (model, view, display) => [...new Set(shownFields(model, view, display))].filter((name) => model.fields[name]?.sortable);
|
|
93
94
|
const isLabelled = (value) => typeof value === "object" && value !== null && "id" in value && "name" in value;
|
|
94
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* A loaded row, its relations as `{ id, name }`, split into the record as the API gives it, the relations' names and
|
|
97
|
+
* the photos it was loaded with.
|
|
98
|
+
*/
|
|
95
99
|
const displayRow = (model, row) => {
|
|
96
100
|
const record = {};
|
|
97
101
|
const labels = {};
|
|
102
|
+
const photos = Array.isArray(row["$photos"]) ? row[photosKey] : void 0;
|
|
98
103
|
for (const [name, value] of Object.entries(row)) {
|
|
99
|
-
if (name === "id" && !Object.hasOwn(model.fields,
|
|
104
|
+
if ((name === "id" || name === "$photos") && !Object.hasOwn(model.fields, name)) continue;
|
|
100
105
|
if (model.fields[name]?.type === "relation" && isLabelled(value)) {
|
|
101
106
|
record[name] = value.id;
|
|
102
107
|
labels[name] = value.name;
|
|
@@ -105,7 +110,8 @@ const displayRow = (model, row) => {
|
|
|
105
110
|
return {
|
|
106
111
|
key: encodeKey(recordKey(model, record)),
|
|
107
112
|
record,
|
|
108
|
-
labels
|
|
113
|
+
labels,
|
|
114
|
+
...photos && { photos }
|
|
109
115
|
};
|
|
110
116
|
};
|
|
111
117
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
1
2
|
import { Button } from "../../primitives/button.js";
|
|
2
3
|
import { humanize } from "../../live/naming.js";
|
|
3
4
|
import { mapCards } from "./list-displays.js";
|
|
@@ -23,7 +24,7 @@ const arrow = "flex size-8 items-center justify-center rounded-full border borde
|
|
|
23
24
|
* its record at the map's bottom, with arrows through the records the marker stands for; closing it or choosing the map
|
|
24
25
|
* elsewhere clears it.
|
|
25
26
|
*/
|
|
26
|
-
const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove, footer = true }) => {
|
|
27
|
+
const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove, footer = true, flush = false }) => {
|
|
27
28
|
const style = useAdminMeta().maps?.style;
|
|
28
29
|
const [chosen, setChosen] = useState();
|
|
29
30
|
const [area, setArea] = useState();
|
|
@@ -55,9 +56,9 @@ const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove,
|
|
|
55
56
|
if (!style) return /* @__PURE__ */ jsx(Problem, { children: "The map display needs a map style: set PROTOBASE_MAP_STYLE_URL." });
|
|
56
57
|
const empty = loaded?.total === 0;
|
|
57
58
|
return /* @__PURE__ */ jsxs("div", {
|
|
58
|
-
className: "flex h-full min-h-[36rem] flex-1 flex-col sm:min-h-[28rem]",
|
|
59
|
+
className: cn("flex h-full min-h-[36rem] flex-1 flex-col sm:min-h-[28rem]", flush && "max-md:-mx-5 max-md:-mb-4 max-md:min-h-0"),
|
|
59
60
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
60
|
-
className: "relative min-h-0 flex-1 overflow-hidden rounded-lg border border-border bg-muted",
|
|
61
|
+
className: cn("relative min-h-0 flex-1 overflow-hidden rounded-lg border border-border bg-muted", flush && "max-md:rounded-none max-md:border-0"),
|
|
61
62
|
children: [
|
|
62
63
|
/* @__PURE__ */ jsx(Suspense, {
|
|
63
64
|
fallback: null,
|
|
@@ -164,7 +165,7 @@ const MapDisplay = ({ model, view, display, filter, orderBy, fields, at, onMove,
|
|
|
164
165
|
})
|
|
165
166
|
]
|
|
166
167
|
}), footer && /* @__PURE__ */ jsx("p", {
|
|
167
|
-
className: "py-3 text-xs text-muted-foreground",
|
|
168
|
+
className: "hidden py-3 text-xs text-muted-foreground md:block",
|
|
168
169
|
children: inView(records.data, plural)
|
|
169
170
|
})]
|
|
170
171
|
});
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
import { Badge } from "../../primitives/badge.js";
|
|
2
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
2
3
|
import { humanize } from "../../live/naming.js";
|
|
3
4
|
import { useProjectUi } from "../pages/project-ui.js";
|
|
4
|
-
import { shownImage } from "../../live/file-values.js";
|
|
5
5
|
import { displayValue } from "../../live/display-value.js";
|
|
6
6
|
import { amountParts, cardFields, filledAmount, filledMeta } from "./list-displays.js";
|
|
7
7
|
import { AppLink } from "../use-app-address.js";
|
|
8
|
-
import { FileImage } from "../../live/file-image.js";
|
|
9
8
|
import { shownWidget } from "../field-types/shown-widget.js";
|
|
10
9
|
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
11
10
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
12
11
|
import { namedIcon } from "../named-icon.js";
|
|
12
|
+
import { FavoriteButton } from "../saved/favorite-button.js";
|
|
13
13
|
import { ValueBadge } from "../value-badge.js";
|
|
14
|
+
import { CardImage } from "./card-image.js";
|
|
14
15
|
import { CardCarousel } from "./card-carousel.js";
|
|
15
16
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
16
17
|
//#region src/app/displays/record-card.tsx
|
|
17
|
-
const CardImage = ({ value }) => {
|
|
18
|
-
const image = shownImage(value, {
|
|
19
|
-
width: 320,
|
|
20
|
-
height: 240
|
|
21
|
-
});
|
|
22
|
-
return /* @__PURE__ */ jsx("span", {
|
|
23
|
-
className: "-mx-3 -mt-3 mb-1.5 block aspect-[4/3] overflow-hidden rounded-t-lg border-b border-border bg-muted",
|
|
24
|
-
children: image && /* @__PURE__ */ jsx(FileImage, {
|
|
25
|
-
image,
|
|
26
|
-
loading: "lazy",
|
|
27
|
-
className: "size-full object-cover"
|
|
28
|
-
})
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
18
|
const MetaValue = ({ icon, children }) => {
|
|
32
19
|
const Icon = namedIcon(icon);
|
|
33
20
|
return /* @__PURE__ */ jsxs("dd", {
|
|
@@ -39,13 +26,13 @@ const MetaValue = ({ icon, children }) => {
|
|
|
39
26
|
});
|
|
40
27
|
};
|
|
41
28
|
/**
|
|
42
|
-
* A record's card of a cards display, opening its page: its image across the top,
|
|
43
|
-
* the display names them, the title over the subtitle, the badge beside it, the first amount with a value in bold,
|
|
44
|
-
* meta fields below, those with a value, the labels of its flags that are true and the footer's fields with a
|
|
45
|
-
* joined by bullets; or, with a `card` component, what it draws of the row, the built-in content again when it
|
|
46
|
-
* missing or throws.
|
|
29
|
+
* A record's card of a cards display, opening its page: its image across the top, swiped or stepped through its photos
|
|
30
|
+
* when the display names them, the title over the subtitle, the badge beside it, the first amount with a value in bold,
|
|
31
|
+
* small meta fields below, those with a value, the labels of its flags that are true and the footer's fields with a
|
|
32
|
+
* value, joined by bullets; or, with a `card` component, what it draws of the row, the built-in content again when it
|
|
33
|
+
* is missing or throws. With a favorite button, a heart in the top right corner saves the record.
|
|
47
34
|
*/
|
|
48
|
-
const RecordCard = ({ model, view, display, row }) => {
|
|
35
|
+
const RecordCard = ({ model, view, display, row, swipe = true }) => {
|
|
49
36
|
const { image, title, subtitle, amount, badge, meta, footer, flags } = cardFields(model, view, display);
|
|
50
37
|
const label = (name) => view?.fields[name]?.label ?? humanize(name);
|
|
51
38
|
const text = (name) => {
|
|
@@ -96,10 +83,12 @@ const RecordCard = ({ model, view, display, row }) => {
|
|
|
96
83
|
const cover = image === void 0 ? void 0 : row.record[image];
|
|
97
84
|
const shownAmount = filledAmount(row, amount);
|
|
98
85
|
const footing = filledMeta(row, footer);
|
|
99
|
-
const
|
|
100
|
-
|
|
86
|
+
const pictured = image !== void 0 || photos !== void 0;
|
|
87
|
+
const favorite = display.favorite;
|
|
88
|
+
const builtIn = (picture) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
89
|
+
pictured && picture,
|
|
101
90
|
/* @__PURE__ */ jsxs("span", {
|
|
102
|
-
className:
|
|
91
|
+
className: `flex items-start justify-between gap-2 ${favorite && !pictured ? "pr-8" : ""}`,
|
|
103
92
|
children: [/* @__PURE__ */ jsx("span", {
|
|
104
93
|
className: "min-w-0 truncate font-medium",
|
|
105
94
|
children: shownValue(title)
|
|
@@ -146,7 +135,7 @@ const RecordCard = ({ model, view, display, row }) => {
|
|
|
146
135
|
}), shownValue(name)] }, name))
|
|
147
136
|
})
|
|
148
137
|
] });
|
|
149
|
-
const card = (
|
|
138
|
+
const card = (picture) => /* @__PURE__ */ jsx(AppLink, {
|
|
150
139
|
to: {
|
|
151
140
|
kind: "record",
|
|
152
141
|
resource: model.name,
|
|
@@ -155,18 +144,23 @@ const RecordCard = ({ model, view, display, row }) => {
|
|
|
155
144
|
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",
|
|
156
145
|
children: Card ? /* @__PURE__ */ jsx(CustomBoundary, {
|
|
157
146
|
name: display.card,
|
|
158
|
-
fallback: builtIn(
|
|
147
|
+
fallback: builtIn(picture),
|
|
159
148
|
children: /* @__PURE__ */ jsx(Card, { row })
|
|
160
|
-
}) : builtIn(
|
|
149
|
+
}) : builtIn(picture)
|
|
161
150
|
});
|
|
162
|
-
return /* @__PURE__ */
|
|
163
|
-
className: photos ? "group/card relative h-full" : "h-full",
|
|
164
|
-
children: photos ? /* @__PURE__ */ jsx(CardCarousel, {
|
|
165
|
-
photos,
|
|
166
|
-
recordKey: row.key,
|
|
151
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
152
|
+
className: photos ? "group/card relative h-full" : favorite ? "relative h-full" : "h-full",
|
|
153
|
+
children: [photos ? /* @__PURE__ */ jsx(CardCarousel, {
|
|
154
|
+
photos: row.photos ?? [],
|
|
167
155
|
cover,
|
|
156
|
+
swipe,
|
|
168
157
|
children: card
|
|
169
|
-
}) : card(cover)
|
|
158
|
+
}) : card(/* @__PURE__ */ jsx(CardImage, { value: cover })), favorite && /* @__PURE__ */ jsx(FavoriteButton, {
|
|
159
|
+
favorite,
|
|
160
|
+
recordKey: row.key,
|
|
161
|
+
target: recordKey(model, row.record),
|
|
162
|
+
className: pictured ? void 0 : "right-1.5 top-1.5"
|
|
163
|
+
})]
|
|
170
164
|
});
|
|
171
165
|
};
|
|
172
166
|
//#endregion
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { keys } from "../../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../../data/api-provider.js";
|
|
3
|
-
import { withLabels } from "../../live/relation-labels.js";
|
|
3
|
+
import { listLabels, withLabels } from "../../live/relation-labels.js";
|
|
4
|
+
import { listPhotos, withPhotos } from "../../live/row-photos.js";
|
|
4
5
|
import { displayRow } from "./list-displays.js";
|
|
5
6
|
import { useAdminMeta } from "../meta-gate.js";
|
|
6
7
|
import { areaFilter, requestZoom } from "../map/map-area.js";
|
|
7
|
-
import { keepPreviousData, useQuery
|
|
8
|
+
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
8
9
|
//#region src/app/displays/use-map-records.ts
|
|
9
10
|
/**
|
|
10
|
-
* The records in the map's area through the list API's `:map`, labelled for the card, or buckets of them when it holds
|
|
11
|
+
* The records in the map's area through the list API's `:map`, labelled for the card and with its photos, or buckets of them when it holds
|
|
11
12
|
* many. A new area or filter keeps the previous answer until its own arrives, and an answer for an area the map has
|
|
12
13
|
* left is never shown.
|
|
13
14
|
*/
|
|
14
15
|
const useMapRecords = ({ model, display, fields, filter, orderBy, area }) => {
|
|
15
16
|
const client = useClient();
|
|
16
|
-
const queryClient = useQueryClient();
|
|
17
17
|
const { resources, views } = useAdminMeta();
|
|
18
18
|
const position = {
|
|
19
19
|
latitude: display.latitude,
|
|
@@ -30,10 +30,12 @@ const useMapRecords = ({ model, display, fields, filter, orderBy, area }) => {
|
|
|
30
30
|
...position,
|
|
31
31
|
zoom,
|
|
32
32
|
filter: inside,
|
|
33
|
-
orderBy
|
|
33
|
+
orderBy,
|
|
34
|
+
labels: listLabels(resources, views, model, fields),
|
|
35
|
+
photos: listPhotos(views[model.name])
|
|
34
36
|
});
|
|
35
37
|
return {
|
|
36
|
-
rows:
|
|
38
|
+
rows: withLabels(resources, model, fields, withPhotos(model, result.items, result.photos), result.labels).map((record) => displayRow(model, record)),
|
|
37
39
|
buckets: result.buckets,
|
|
38
40
|
total: result.totalSize
|
|
39
41
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { humanize } from "../../live/naming.js";
|
|
2
2
|
import { displayRow } from "../displays/list-displays.js";
|
|
3
3
|
import { columnsFromModel } from "../../live/columns-from-model.js";
|
|
4
|
-
import { withFieldTypeCells } from "../field-types/field-type-cells.js";
|
|
5
4
|
import { scopeOf } from "../pages/use-layout-data.js";
|
|
5
|
+
import { withFieldTypeCells } from "../field-types/field-type-cells.js";
|
|
6
6
|
import { FieldValue } from "../pages/field-value.js";
|
|
7
7
|
import { usePathValue } from "../pages/use-path-value.js";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
import { keys } from "../../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../../data/api-provider.js";
|
|
3
3
|
import { recordId } from "../../live/model-helpers.js";
|
|
4
|
-
import { withLabels } from "../../live/relation-labels.js";
|
|
4
|
+
import { listLabels, withLabels } from "../../live/relation-labels.js";
|
|
5
|
+
import { listPhotos, withPhotos } from "../../live/row-photos.js";
|
|
5
6
|
import { useAdminMeta } from "../meta-gate.js";
|
|
6
|
-
import { keepPreviousData, useQuery
|
|
7
|
+
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
7
8
|
//#region src/app/embedded-list/use-embedded-rows.ts
|
|
8
9
|
/**
|
|
9
10
|
* The first `limit` records in `orderBy` order, as the list page's table and displays get them: relations among
|
|
10
|
-
* `fields` as `{ id, name }
|
|
11
|
+
* `fields` as `{ id, name }`, named in the same call, with the photos of its cards. A filter through a join resource may hold hundreds of keys, so it
|
|
12
|
+
* goes in a JSON body.
|
|
11
13
|
*/
|
|
12
14
|
const useEmbeddedRows = ({ model, filter, orderBy, limit, fields }) => {
|
|
13
15
|
const client = useClient();
|
|
14
|
-
const queryClient = useQueryClient();
|
|
15
16
|
const { resources, views } = useAdminMeta();
|
|
16
17
|
return useQuery({
|
|
17
18
|
queryKey: [...keys.list(model.name, filter, orderBy, limit, "embedded"), fields.join(",")],
|
|
18
19
|
placeholderData: keepPreviousData,
|
|
19
20
|
queryFn: async () => {
|
|
21
|
+
const labels = listLabels(resources, views, model, fields);
|
|
22
|
+
const photos = listPhotos(views[model.name]);
|
|
20
23
|
const page = await client.search(model.name, {
|
|
21
24
|
filter: filter || void 0,
|
|
22
25
|
orderBy: orderBy || void 0,
|
|
23
|
-
pageSize: limit
|
|
26
|
+
pageSize: limit,
|
|
27
|
+
labels,
|
|
28
|
+
photos
|
|
24
29
|
});
|
|
25
|
-
return
|
|
30
|
+
return withLabels(resources, model, fields, withPhotos(model, page.items, page.photos), page.labels).map((row, index) => ({
|
|
26
31
|
...row,
|
|
27
32
|
id: recordId(model, page.items[index])
|
|
28
33
|
}));
|