@protobase/ui 0.25.5 → 0.25.7
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/pages/blocks/action.js +8 -4
- package/dist/app/related-cards/clamped.js +43 -0
- package/dist/app/related-cards/messages.js +18 -0
- package/dist/app/related-cards/related-card.js +66 -0
- package/dist/app/related-cards/related-cards.js +85 -0
- package/dist/app/related-section.js +24 -7
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-DUhyxZ0r.js +69 -0
- package/dist/shell/assets/index-BrgzNBVN.css +2 -0
- package/dist/shell/assets/{index-BPi_cyLV.js → index-Df3nrVFB.js} +1 -1
- package/dist/shell/assets/{records-map-EQVZhkY9.js → records-map-BvwXRw3F.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-BrUtMzdS.js → shared-protobase-ui-75Z0WZsK.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/package.json +4 -4
- package/dist/shell/assets/app-pH0OC9QP.js +0 -69
- package/dist/shell/assets/index-Bkc7w8I4.css +0 -2
|
@@ -6,9 +6,11 @@ import { propsOf } from "./block-props.js";
|
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/app/pages/blocks/action.tsx
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A button for a named action of `resource`'s view, by default of the record around it, on that record, asking first
|
|
11
|
+
* when the action has `confirm` or `inputs`; nothing when the view has no such action for the user.
|
|
12
|
+
*/
|
|
13
|
+
const ActionButton = ({ name, resource, label, variant }) => {
|
|
12
14
|
const scope = useLayoutRecord();
|
|
13
15
|
const action = useRunAction(resource ?? scope?.model.name ?? "", name, resource === void 0 || resource === scope?.model.name ? scope : void 0);
|
|
14
16
|
const [asking, setAsking] = useState(false);
|
|
@@ -38,5 +40,7 @@ const ActionBlock = ({ node }) => {
|
|
|
38
40
|
}
|
|
39
41
|
})] });
|
|
40
42
|
};
|
|
43
|
+
/** An Action block: its button. */
|
|
44
|
+
const ActionBlock = ({ node }) => /* @__PURE__ */ jsx(ActionButton, { ...propsOf(node) });
|
|
41
45
|
//#endregion
|
|
42
|
-
export { ActionBlock };
|
|
46
|
+
export { ActionBlock, ActionButton };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { Button } from "../../primitives/button.js";
|
|
3
|
+
import { text } from "./messages.js";
|
|
4
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/app/related-cards/clamped.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Its content cut off at a few lines' height with "Show more" under it when it is taller, which shows it all; the cut
|
|
9
|
+
* hides the rest rather than scrolling it, so the page keeps its one scrollbar.
|
|
10
|
+
*/
|
|
11
|
+
const Clamped = ({ children }) => {
|
|
12
|
+
const content = useRef(null);
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const [taller, setTaller] = useState(false);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
const box = content.current;
|
|
17
|
+
if (!box) return;
|
|
18
|
+
const measure = () => setTaller(box.scrollHeight > box.parentElement.clientHeight + 1);
|
|
19
|
+
measure();
|
|
20
|
+
const observer = new ResizeObserver(measure);
|
|
21
|
+
observer.observe(box);
|
|
22
|
+
return () => observer.disconnect();
|
|
23
|
+
}, [open]);
|
|
24
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
25
|
+
className: "flex flex-col items-start gap-1",
|
|
26
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
27
|
+
className: cn("w-full min-w-0", !open && "max-h-40 overflow-hidden"),
|
|
28
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
29
|
+
ref: content,
|
|
30
|
+
children
|
|
31
|
+
})
|
|
32
|
+
}), (taller || open) && /* @__PURE__ */ jsx(Button, {
|
|
33
|
+
size: "sm",
|
|
34
|
+
variant: "ghost",
|
|
35
|
+
className: "-ml-2",
|
|
36
|
+
"aria-expanded": open,
|
|
37
|
+
onClick: () => setOpen(!open),
|
|
38
|
+
children: open ? text("showLess") : text("showMore")
|
|
39
|
+
})]
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { Clamped };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineMessages } from "../../locale/messages.js";
|
|
2
|
+
//#region src/app/related-cards/messages.ts
|
|
3
|
+
const text = defineMessages({
|
|
4
|
+
en: {
|
|
5
|
+
showMore: "Show more",
|
|
6
|
+
showLess: "Show less",
|
|
7
|
+
couldNotLoad: "Could not load",
|
|
8
|
+
nothingYet: "Nothing here yet."
|
|
9
|
+
},
|
|
10
|
+
nl: {
|
|
11
|
+
showMore: "Meer tonen",
|
|
12
|
+
showLess: "Minder tonen",
|
|
13
|
+
couldNotLoad: "Kon niet laden",
|
|
14
|
+
nothingYet: "Hier staat nog niets."
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
//#endregion
|
|
18
|
+
export { text };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Skeleton } from "../../primitives/skeleton.js";
|
|
2
|
+
import { recordKey, recordTitle } from "../../live/model-helpers.js";
|
|
3
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
4
|
+
import { AppLink } from "../use-app-address.js";
|
|
5
|
+
import { RecordScope } from "../pages/record-scope.js";
|
|
6
|
+
import { ActionButton } from "../pages/blocks/action.js";
|
|
7
|
+
import { FieldValue } from "../pages/field-value.js";
|
|
8
|
+
import { usePathValue } from "../pages/use-path-value.js";
|
|
9
|
+
import { fieldLabel } from "../pages/blocks/field.js";
|
|
10
|
+
import { Clamped } from "./clamped.js";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
//#region src/app/related-cards/related-card.tsx
|
|
13
|
+
const CardField = ({ scope, path }) => {
|
|
14
|
+
const { views } = useAdminMeta();
|
|
15
|
+
const value = usePathValue(scope, path);
|
|
16
|
+
if (value.state === "missing") return null;
|
|
17
|
+
const label = fieldLabel(path, value.state === "ready" ? views[value.model.name]?.fields[value.field.name]?.label : void 0);
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: "min-w-0",
|
|
20
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
21
|
+
className: "text-body text-muted-foreground",
|
|
22
|
+
children: label
|
|
23
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
24
|
+
className: "mt-0.5",
|
|
25
|
+
children: value.state === "loading" ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) : /* @__PURE__ */ jsx(Clamped, { children: /* @__PURE__ */ jsx(FieldValue, {
|
|
26
|
+
model: value.model,
|
|
27
|
+
field: value.field,
|
|
28
|
+
value: value.value
|
|
29
|
+
}) })
|
|
30
|
+
})]
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A related record as a card: its title linking to its page, its `fields` under their labels and the buttons of its
|
|
35
|
+
* view's `actions`, run on it as everywhere else.
|
|
36
|
+
*/
|
|
37
|
+
const RelatedCard = ({ scope, view, fields, actions }) => /* @__PURE__ */ jsxs("li", {
|
|
38
|
+
className: "flex min-w-0 flex-col gap-3 rounded-lg border border-border bg-surface p-4",
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx(AppLink, {
|
|
41
|
+
to: {
|
|
42
|
+
kind: "record",
|
|
43
|
+
resource: scope.model.name,
|
|
44
|
+
key: recordKey(scope.model, scope.record)
|
|
45
|
+
},
|
|
46
|
+
className: "self-start font-medium text-primary-text hover:underline",
|
|
47
|
+
children: recordTitle(scope.model, view, scope.record, scope.key)
|
|
48
|
+
}),
|
|
49
|
+
fields.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
50
|
+
className: "flex flex-col gap-3",
|
|
51
|
+
children: fields.map((path) => /* @__PURE__ */ jsx(CardField, {
|
|
52
|
+
scope,
|
|
53
|
+
path
|
|
54
|
+
}, path))
|
|
55
|
+
}),
|
|
56
|
+
actions.length > 0 && /* @__PURE__ */ jsx(RecordScope, {
|
|
57
|
+
value: scope,
|
|
58
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
59
|
+
className: "flex flex-wrap items-center gap-2",
|
|
60
|
+
children: actions.map((name) => /* @__PURE__ */ jsx(ActionButton, { name }, name))
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
//#endregion
|
|
66
|
+
export { RelatedCard };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useIsXs } from "../../lib/use-media-query.js";
|
|
2
|
+
import { Skeleton } from "../../primitives/skeleton.js";
|
|
3
|
+
import { humanize } from "../../live/naming.js";
|
|
4
|
+
import { defaultOrderBy } from "../list-order.js";
|
|
5
|
+
import { AppLink } from "../use-app-address.js";
|
|
6
|
+
import { ErrorBanner } from "../error-banner.js";
|
|
7
|
+
import { scopeOf, useCount, useListPage } from "../pages/use-layout-data.js";
|
|
8
|
+
import { embeddedDisplay, viewAllAddress } from "../embedded-list/embedded-display.js";
|
|
9
|
+
import { text } from "../embedded-list/messages.js";
|
|
10
|
+
import { cutOffNote } from "../embedded-list/cut-off-note.js";
|
|
11
|
+
import { text as text$1 } from "./messages.js";
|
|
12
|
+
import { RelatedCard } from "./related-card.js";
|
|
13
|
+
import { ArrowRight } from "lucide-react";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
//#region src/app/related-cards/related-cards.tsx
|
|
16
|
+
/**
|
|
17
|
+
* The first `limit` records of a list as cards stacked full width, each with its fields and its actions' buttons, and a
|
|
18
|
+
* link to the whole list beside a note such as "The first 10 of 37." when there are more.
|
|
19
|
+
*/
|
|
20
|
+
const RelatedCards = ({ model, view, filter, sort, fields, actions, limit, display, empty }) => {
|
|
21
|
+
const xs = useIsXs();
|
|
22
|
+
const orderBy = sort || defaultOrderBy(model, view);
|
|
23
|
+
const page = useListPage(model, {
|
|
24
|
+
filter,
|
|
25
|
+
sort: orderBy,
|
|
26
|
+
pageSize: limit
|
|
27
|
+
});
|
|
28
|
+
const count = useCount(model, filter);
|
|
29
|
+
if (page.isPending) return /* @__PURE__ */ jsx("div", {
|
|
30
|
+
className: "flex flex-col gap-3",
|
|
31
|
+
children: Array.from({ length: 2 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }, index))
|
|
32
|
+
});
|
|
33
|
+
if (page.error) return /* @__PURE__ */ jsx(ErrorBanner, {
|
|
34
|
+
title: text$1("couldNotLoad"),
|
|
35
|
+
error: page.error
|
|
36
|
+
});
|
|
37
|
+
const rows = page.data.items;
|
|
38
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
39
|
+
className: "text-muted-foreground",
|
|
40
|
+
children: empty ?? text$1("nothingYet")
|
|
41
|
+
});
|
|
42
|
+
const note = count.data === void 0 ? void 0 : cutOffNote(rows.length, count.data, false);
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
44
|
+
className: "flex flex-col gap-3",
|
|
45
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
46
|
+
className: "flex flex-col gap-3",
|
|
47
|
+
children: rows.map((row) => {
|
|
48
|
+
const scope = scopeOf(model, row);
|
|
49
|
+
return /* @__PURE__ */ jsx(RelatedCard, {
|
|
50
|
+
scope,
|
|
51
|
+
view,
|
|
52
|
+
fields,
|
|
53
|
+
actions
|
|
54
|
+
}, scope.key);
|
|
55
|
+
})
|
|
56
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
57
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
58
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
59
|
+
className: "text-xs text-muted-foreground",
|
|
60
|
+
children: note
|
|
61
|
+
}), /* @__PURE__ */ jsxs(AppLink, {
|
|
62
|
+
to: viewAllAddress(model, view, xs, {
|
|
63
|
+
filter,
|
|
64
|
+
orderBy,
|
|
65
|
+
display: embeddedDisplay(view, xs, { preferred: display })
|
|
66
|
+
}),
|
|
67
|
+
className: "flex items-center gap-1 font-medium text-primary-text hover:underline",
|
|
68
|
+
children: [
|
|
69
|
+
text("viewAll"),
|
|
70
|
+
" ",
|
|
71
|
+
/* @__PURE__ */ jsx("span", {
|
|
72
|
+
className: "sr-only",
|
|
73
|
+
children: (view?.names?.plural ?? humanize(model.name)).toLowerCase()
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ jsx(ArrowRight, {
|
|
76
|
+
className: "size-3.5",
|
|
77
|
+
"aria-hidden": true
|
|
78
|
+
})
|
|
79
|
+
]
|
|
80
|
+
})]
|
|
81
|
+
})]
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
export { RelatedCards };
|
|
@@ -14,6 +14,7 @@ import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
|
|
|
14
14
|
import { text } from "./record-messages.js";
|
|
15
15
|
import { HandSortedList } from "./hand-order/hand-sorted-list.js";
|
|
16
16
|
import { ReorderButton } from "./hand-order/reorder-button.js";
|
|
17
|
+
import { RelatedCards } from "./related-cards/related-cards.js";
|
|
17
18
|
import { useState } from "react";
|
|
18
19
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
20
|
import { useQuery } from "@tanstack/react-query";
|
|
@@ -78,7 +79,7 @@ const Empty = ({ empty }) => /* @__PURE__ */ jsx("p", {
|
|
|
78
79
|
});
|
|
79
80
|
/**
|
|
80
81
|
* A record page section listing the first records of another resource that point at this record, directly or through
|
|
81
|
-
* a join resource, in the list page's table or display, with a link to them all in the list. When they are ordered by
|
|
82
|
+
* a join resource, in the list page's table or display, or as cards with their actions, with a link to them all in the list. When they are ordered by
|
|
82
83
|
* hand within this record, **Reorder** shows them all in their order, to be moved by their drag handles.
|
|
83
84
|
*/
|
|
84
85
|
const RelatedSection = ({ item, value, owner }) => {
|
|
@@ -100,6 +101,25 @@ const RelatedSection = ({ item, value, owner }) => {
|
|
|
100
101
|
if (item.hideEmpty && (match.isPending || match.filter === null || counting && (count.isPending || count.data === 0))) return null;
|
|
101
102
|
const opens = relatedOpens(item, model, resources);
|
|
102
103
|
const columns = item.columns ?? listColumnNames(model, view).filter((name) => name !== item.field);
|
|
104
|
+
const listed = (list) => match.isPending ? /* @__PURE__ */ jsx(Loading, {}) : match.error ? /* @__PURE__ */ jsx(ErrorBanner, {
|
|
105
|
+
title: text("couldNotLoadRelated"),
|
|
106
|
+
error: match.error
|
|
107
|
+
}) : match.filter === null || match.filter === void 0 ? /* @__PURE__ */ jsx(Empty, { empty: item.empty }) : list(match.filter);
|
|
108
|
+
if (item.presentation === "cards") return /* @__PURE__ */ jsx(Section, {
|
|
109
|
+
title: item.title,
|
|
110
|
+
help: item.help,
|
|
111
|
+
children: listed((filter) => /* @__PURE__ */ jsx(RelatedCards, {
|
|
112
|
+
model,
|
|
113
|
+
view,
|
|
114
|
+
filter,
|
|
115
|
+
sort: item.sort ?? "",
|
|
116
|
+
fields: columns,
|
|
117
|
+
actions: item.actions ?? [],
|
|
118
|
+
limit: item.limit ?? 10,
|
|
119
|
+
display: item.display,
|
|
120
|
+
empty: item.empty
|
|
121
|
+
}))
|
|
122
|
+
});
|
|
103
123
|
const handOrder = relatedHandOrder(item, model, permissions);
|
|
104
124
|
const displaySwitch = /* @__PURE__ */ jsx(EmbeddedDisplaySwitch, {
|
|
105
125
|
view,
|
|
@@ -130,20 +150,17 @@ const RelatedSection = ({ item, value, owner }) => {
|
|
|
130
150
|
title: item.title,
|
|
131
151
|
help: item.help,
|
|
132
152
|
actions,
|
|
133
|
-
children:
|
|
134
|
-
title: text("couldNotLoadRelated"),
|
|
135
|
-
error: match.error
|
|
136
|
-
}) : match.filter === null || match.filter === void 0 ? /* @__PURE__ */ jsx(Empty, { empty: item.empty }) : /* @__PURE__ */ jsx(EmbeddedList, {
|
|
153
|
+
children: listed((filter) => /* @__PURE__ */ jsx(EmbeddedList, {
|
|
137
154
|
model,
|
|
138
155
|
view,
|
|
139
|
-
filter
|
|
156
|
+
filter,
|
|
140
157
|
sort: item.sort ?? "",
|
|
141
158
|
columns,
|
|
142
159
|
limit: item.limit ?? 10,
|
|
143
160
|
display,
|
|
144
161
|
empty: item.empty,
|
|
145
162
|
opens
|
|
146
|
-
},
|
|
163
|
+
}, filter))
|
|
147
164
|
});
|
|
148
165
|
};
|
|
149
166
|
//#endregion
|
package/dist/package.js
CHANGED