@protobase/ui 0.22.3 → 0.22.4
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/charts/chart-view.js +2 -0
- package/dist/app/charts/group-bars.js +3 -6
- package/dist/app/charts/group-donut.js +3 -6
- package/dist/app/current-address.js +4 -4
- package/dist/app/displays/board-allowed.js +26 -0
- package/dist/app/displays/board-display.js +2 -1
- package/dist/app/list-link.js +23 -0
- package/dist/app/pages/blocks/stat.js +3 -5
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BM5iQ8yZ.js +69 -0
- package/dist/shell/assets/{index-K4WBo5hS.js → index-DylcQP59.js} +1 -1
- package/dist/shell/assets/{maplibre-CPwgBgXC.js → maplibre-CY0vkBw4.js} +1 -1
- package/dist/shell/assets/{markdown-content-kdNPOEJy.js → markdown-content-D3r5sNwd.js} +1 -1
- package/dist/shell/assets/{record-map-C57pIfol.js → record-map-C9XvJmY1.js} +1 -1
- package/dist/shell/assets/{records-map-B8_M8kjM.js → records-map-BtQTl7lD.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-BRE7ZUBU.js → shared-protobase-ui-BIRhZOyK.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-3MBLkJAB.js +0 -69
|
@@ -65,10 +65,12 @@ const ChartView = ({ model, view, groupBy, type = "bar", sum, average, filter =
|
|
|
65
65
|
const slices = valueSlices(groups, field, view?.fields[groupBy], measure, filter);
|
|
66
66
|
return type === "donut" ? /* @__PURE__ */ jsx(GroupDonut, {
|
|
67
67
|
resource: model.name,
|
|
68
|
+
view,
|
|
68
69
|
slices,
|
|
69
70
|
measure
|
|
70
71
|
}) : /* @__PURE__ */ jsx(GroupBars, {
|
|
71
72
|
resource: model.name,
|
|
73
|
+
view,
|
|
72
74
|
slices,
|
|
73
75
|
measure
|
|
74
76
|
});
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import { AppLink } from "../use-app-address.js";
|
|
2
2
|
import { measureText } from "./chart-groups.js";
|
|
3
|
+
import { filteredListAddress } from "../list-link.js";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/app/charts/group-bars.tsx
|
|
5
6
|
/**
|
|
6
7
|
* A ranked list of a field's values, each a bar as long as its number and a link to its records. Bars in rows read on
|
|
7
8
|
* a phone as well as on a desk, and carry their label and value as text, so no colour has to say what a bar is.
|
|
8
9
|
*/
|
|
9
|
-
const GroupBars = ({ resource, slices, measure }) => {
|
|
10
|
+
const GroupBars = ({ resource, view, slices, measure }) => {
|
|
10
11
|
const largest = Math.max(0, ...slices.map((slice) => slice.value ?? 0));
|
|
11
12
|
return /* @__PURE__ */ jsx("ul", {
|
|
12
13
|
className: "flex flex-col gap-1",
|
|
13
14
|
children: slices.map((slice) => {
|
|
14
15
|
const share = largest > 0 && slice.value !== null ? Math.max(0, slice.value) / largest : 0;
|
|
15
16
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(AppLink, {
|
|
16
|
-
to:
|
|
17
|
-
kind: "list",
|
|
18
|
-
resource,
|
|
19
|
-
filter: slice.filter
|
|
20
|
-
},
|
|
17
|
+
to: filteredListAddress(view, resource, slice.filter),
|
|
21
18
|
className: "group grid grid-cols-[minmax(6rem,35%)_1fr_auto] items-center gap-3 rounded-md px-1 py-1 hover:bg-muted",
|
|
22
19
|
title: measure.kind === "count" ? void 0 : `${measureText(slice.count, { kind: "count" })} records`,
|
|
23
20
|
children: [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppLink } from "../use-app-address.js";
|
|
2
2
|
import { measureText } from "./chart-groups.js";
|
|
3
|
+
import { filteredListAddress } from "../list-link.js";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
|
5
6
|
//#region src/app/charts/group-donut.tsx
|
|
@@ -22,7 +23,7 @@ const SliceTooltip = ({ active, payload, measure }) => {
|
|
|
22
23
|
* A donut of how a total shares out over a field's values, with a legend that names each, gives its number and share,
|
|
23
24
|
* and links to its records: the legend, not the colour, says which slice is which.
|
|
24
25
|
*/
|
|
25
|
-
const GroupDonut = ({ resource, slices, measure }) => {
|
|
26
|
+
const GroupDonut = ({ resource, view, slices, measure }) => {
|
|
26
27
|
const drawn = slices.filter((slice) => slice.value !== null && slice.value > 0);
|
|
27
28
|
const whole = drawn.reduce((sum, slice) => sum + slice.value, 0);
|
|
28
29
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -51,11 +52,7 @@ const GroupDonut = ({ resource, slices, measure }) => {
|
|
|
51
52
|
}), /* @__PURE__ */ jsx("ul", {
|
|
52
53
|
className: "flex w-full min-w-0 flex-col gap-0.5",
|
|
53
54
|
children: slices.map((slice) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(AppLink, {
|
|
54
|
-
to:
|
|
55
|
-
kind: "list",
|
|
56
|
-
resource,
|
|
57
|
-
filter: slice.filter
|
|
58
|
-
},
|
|
55
|
+
to: filteredListAddress(view, resource, slice.filter),
|
|
59
56
|
className: "group flex items-center gap-2 rounded-md px-1 py-0.5 hover:bg-muted",
|
|
60
57
|
children: [
|
|
61
58
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -24,7 +24,8 @@ const listedResource = (resources, views, resource) => {
|
|
|
24
24
|
const listDefaults = (model, view, xs = false) => ({
|
|
25
25
|
display: model ? listDefaultDisplay(model, view, xs) : defaultDisplay(view, xs),
|
|
26
26
|
orderBy: model ? defaultOrderBy(model, view) : "",
|
|
27
|
-
filterLayout: "bar"
|
|
27
|
+
filterLayout: "bar",
|
|
28
|
+
filter: view?.list?.filter ?? ""
|
|
28
29
|
});
|
|
29
30
|
/** The address as read from the location, with a list's defaults filled in and a record's key as text. */
|
|
30
31
|
const currentAddress = (read, defaults) => {
|
|
@@ -38,11 +39,10 @@ const currentAddress = (read, defaults) => {
|
|
|
38
39
|
if (read?.kind !== "list") return read;
|
|
39
40
|
return {
|
|
40
41
|
...defaults(read.resource),
|
|
41
|
-
filter: "",
|
|
42
42
|
...read
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
|
-
/** The list address after `change`, leaving out every value that is empty or its default, so `/<resource>` stays clean. */
|
|
45
|
+
/** The list address after `change`, leaving out every value that is empty or its default (a cleared filter is written as an empty `filter=` when the default is not empty), so `/<resource>` stays clean. */
|
|
46
46
|
const changedList = (read, change, defaults) => {
|
|
47
47
|
const next = {
|
|
48
48
|
...read,
|
|
@@ -52,7 +52,7 @@ const changedList = (read, change, defaults) => {
|
|
|
52
52
|
return {
|
|
53
53
|
kind: "list",
|
|
54
54
|
resource: read.resource,
|
|
55
|
-
...
|
|
55
|
+
...differs("filter") && { filter: next.filter },
|
|
56
56
|
...differs("display") && { display: next.display },
|
|
57
57
|
...differs("orderBy") && { orderBy: next.orderBy },
|
|
58
58
|
...differs("filterLayout") && { filterLayout: next.filterLayout },
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { parseFilter } from "@protobase/schema";
|
|
2
|
+
//#region src/app/displays/board-allowed.ts
|
|
3
|
+
const flatten = (expr) => expr.kind === "and" ? expr.args.flatMap(flatten) : [expr];
|
|
4
|
+
const text = (value) => value.kind === "string" ? String(value.value) : void 0;
|
|
5
|
+
const allowedBy = (clause, by) => {
|
|
6
|
+
if (clause.kind === "compare" && clause.op === "=" && clause.field.path.join(".") === by) {
|
|
7
|
+
const value = text(clause.value);
|
|
8
|
+
return value === void 0 ? void 0 : [value];
|
|
9
|
+
}
|
|
10
|
+
if (clause.kind === "in" && clause.field.path.join(".") === by) {
|
|
11
|
+
const values = clause.values.map(text);
|
|
12
|
+
return values.every((value) => value !== void 0) ? values : void 0;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The columns of a board `filter` does not rule out: those its top-level `by = "v"` and `in(by, ...)` clauses allow. Any
|
|
17
|
+
* other clause leaves every column, so a column is left out only when no record in it can match.
|
|
18
|
+
*/
|
|
19
|
+
const columnsAllowed = (filter, by, columns) => {
|
|
20
|
+
const parsed = parseFilter(filter);
|
|
21
|
+
if (!parsed.ok || !parsed.ast) return columns;
|
|
22
|
+
const clauses = flatten(parsed.ast).map((clause) => allowedBy(clause, by)).filter((values) => values !== void 0);
|
|
23
|
+
return columns.filter((column) => clauses.every((values) => column.value !== null && values.includes(column.value)));
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { columnsAllowed };
|
|
@@ -4,6 +4,7 @@ import { can } from "../permissions.js";
|
|
|
4
4
|
import { RecordCard } from "./record-card.js";
|
|
5
5
|
import { collision, columnCoordinates, columnId } from "./board-dnd.js";
|
|
6
6
|
import { BoardColumnView } from "./board-column.js";
|
|
7
|
+
import { columnsAllowed } from "./board-allowed.js";
|
|
7
8
|
import { useBoardMove } from "./use-board-move.js";
|
|
8
9
|
import { useRef, useState } from "react";
|
|
9
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -14,7 +15,7 @@ import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useS
|
|
|
14
15
|
* a card dragged, or moved with the keyboard, to another column saves the record with that column's value.
|
|
15
16
|
*/
|
|
16
17
|
const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit }) => {
|
|
17
|
-
const columns = boardColumns(model, view, display);
|
|
18
|
+
const columns = columnsAllowed(filter, display.by, boardColumns(model, view, display));
|
|
18
19
|
const card = boardCard(model, view, display);
|
|
19
20
|
const permissions = usePermissions(model.name);
|
|
20
21
|
const movable = can(permissions, "update") && !model.fields[display.by]?.readOnly;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/app/list-link.ts
|
|
2
|
+
/**
|
|
3
|
+
* The list of `resource` opened on exactly the records `filter` counted or showed: with no filter, an empty one when
|
|
4
|
+
* the view's list opens on a default filter, so the link does not narrow what it counted; otherwise a clean path.
|
|
5
|
+
*/
|
|
6
|
+
const filteredListAddress = (view, resource, filter) => {
|
|
7
|
+
if (filter) return {
|
|
8
|
+
kind: "list",
|
|
9
|
+
resource,
|
|
10
|
+
filter
|
|
11
|
+
};
|
|
12
|
+
if (view?.list?.filter) return {
|
|
13
|
+
kind: "list",
|
|
14
|
+
resource,
|
|
15
|
+
filter: ""
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
kind: "list",
|
|
19
|
+
resource
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { filteredListAddress };
|
|
@@ -5,12 +5,14 @@ import { useLayoutRecord } from "../record-scope.js";
|
|
|
5
5
|
import { useCount } from "../use-layout-data.js";
|
|
6
6
|
import { propsOf } from "./block-props.js";
|
|
7
7
|
import { CardFrame } from "./card-frame.js";
|
|
8
|
+
import { filteredListAddress } from "../../list-link.js";
|
|
8
9
|
import { FieldValue } from "../field-value.js";
|
|
9
10
|
import { usePathValue } from "../use-path-value.js";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
12
|
//#region src/app/pages/blocks/stat.tsx
|
|
12
13
|
const big = "text-2xl font-semibold tracking-tight tabular-nums";
|
|
13
14
|
const Count = ({ model, filter }) => {
|
|
15
|
+
const { views } = useAdminMeta();
|
|
14
16
|
const count = useCount(model, filter);
|
|
15
17
|
if (count.isPending) return /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-16" });
|
|
16
18
|
if (count.error) return /* @__PURE__ */ jsx("span", {
|
|
@@ -18,11 +20,7 @@ const Count = ({ model, filter }) => {
|
|
|
18
20
|
children: "Could not count"
|
|
19
21
|
});
|
|
20
22
|
return /* @__PURE__ */ jsx(AppLink, {
|
|
21
|
-
to:
|
|
22
|
-
kind: "list",
|
|
23
|
-
resource: model.name,
|
|
24
|
-
filter
|
|
25
|
-
},
|
|
23
|
+
to: filteredListAddress(views[model.name], model.name, filter),
|
|
26
24
|
className: `${big} hover:text-primary-text after:absolute after:inset-0 after:content-['']`,
|
|
27
25
|
children: count.data.toLocaleString("en-IE")
|
|
28
26
|
});
|
package/dist/package.js
CHANGED