@protobase/ui 0.19.16 → 0.20.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.
- package/dist/app/charts/chart-groups.js +52 -0
- package/dist/app/charts/chart-view.js +75 -0
- package/dist/app/charts/group-bars.js +49 -0
- package/dist/app/charts/group-donut.js +84 -0
- package/dist/app/charts/period-chart.js +174 -0
- package/dist/app/list-group-chart.js +37 -0
- package/dist/app/list-page.js +10 -3
- package/dist/app/pages/blocks/chart.js +26 -0
- package/dist/app/pages/render-node.js +2 -0
- package/dist/app/relation-options.js +44 -0
- package/dist/app/relation-picker.js +114 -35
- package/dist/data/api-provider.d.ts +2 -0
- package/dist/data/query-keys.js +5 -0
- package/dist/data/use-aggregates.js +14 -1
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-B5h_q_JL.js +69 -0
- package/dist/shell/assets/{index-CmmYBR7Y.js → index-B0veahji.js} +1 -1
- package/dist/shell/assets/index-BZmb28OT.css +2 -0
- package/dist/shell/assets/{maplibre-Siom2Zwj.js → maplibre-CbjT6RKG.js} +1 -1
- package/dist/shell/assets/{markdown-content-ChyGEubR.js → markdown-content-BjRGtSOA.js} +1 -1
- package/dist/shell/assets/{record-map-B_QnHF99.js → record-map-rmkTnAs8.js} +1 -1
- package/dist/shell/assets/{records-map-CcHhEDBC.js → records-map-D1vLGY06.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-zRmreKZS.js → shared-protobase-ui-PnkJqBqB.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/tokens.css +17 -0
- package/package.json +4 -4
- package/dist/shell/assets/app-xQmLJM07.js +0 -69
- package/dist/shell/assets/index-CmJl5dIO.css +0 -2
|
@@ -1,63 +1,142 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
1
2
|
import { Input } from "../primitives/input.js";
|
|
2
|
-
import { useClient } from "../data/api-provider.js";
|
|
3
3
|
import { labelFieldOf } from "../live/model-helpers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { humanize } from "../live/naming.js";
|
|
5
|
+
import { useList } from "../data/use-list.js";
|
|
6
|
+
import { relationOptionsQuery, titleMatches } from "./relation-options.js";
|
|
7
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
8
|
+
import { ApiError } from "@protobase/client";
|
|
5
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { useQuery } from "@tanstack/react-query";
|
|
7
|
-
import { createWhere, printFilter } from "@protobase/schema";
|
|
8
10
|
//#region src/app/relation-picker.tsx
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
/** Loads the next page while `sentinel` is in view of the list; it re-arms after each page, in case it still is. */
|
|
12
|
+
const useLoadMoreInView = (list, sentinel, loaded, loadMore) => {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!list || !sentinel) return;
|
|
15
|
+
const observer = new IntersectionObserver((entries) => entries.some((entry) => entry.isIntersecting) && loadMore(), { root: list });
|
|
16
|
+
observer.observe(sentinel);
|
|
17
|
+
return () => observer.disconnect();
|
|
18
|
+
}, [
|
|
19
|
+
list,
|
|
20
|
+
sentinel,
|
|
21
|
+
loaded,
|
|
22
|
+
loadMore
|
|
23
|
+
]);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Picks a record of another resource: type to narrow the records by their search fields or title, scroll for more, and
|
|
27
|
+
* choose with the mouse or with the arrow keys and Enter; Escape closes the list.
|
|
28
|
+
*/
|
|
11
29
|
const RelationPicker = ({ target, view, value, label, invalid, onChange }) => {
|
|
12
|
-
const client = useClient();
|
|
13
30
|
const [text, setText] = useState("");
|
|
14
31
|
const [open, setOpen] = useState(false);
|
|
32
|
+
const [active, setActive] = useState(-1);
|
|
33
|
+
const [list, setList] = useState(null);
|
|
34
|
+
const [sentinel, setSentinel] = useState(null);
|
|
35
|
+
const listId = useId();
|
|
15
36
|
const keyField = target.primaryKey[0];
|
|
16
37
|
const labelField = labelFieldOf(target, view);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
const [refused, setRefused] = useState(false);
|
|
39
|
+
const query = relationOptionsQuery(target, labelField, text, refused);
|
|
40
|
+
const rows = useList(target.name, {
|
|
41
|
+
filter: query.filter,
|
|
42
|
+
fields: [keyField, labelField],
|
|
43
|
+
enabled: open
|
|
44
|
+
});
|
|
45
|
+
const expensive = query.filter !== void 0 && rows.error instanceof ApiError && rows.error.slug === "expensive-query";
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (expensive) setRefused(true);
|
|
48
|
+
}, [expensive]);
|
|
49
|
+
const items = query.local ? rows.items.filter((item) => titleMatches(item[labelField], text)) : rows.items;
|
|
50
|
+
const more = Boolean(rows.hasNextPage);
|
|
51
|
+
const loading = rows.isPending || rows.isFetchingNextPage;
|
|
52
|
+
const { fetchNextPage } = rows;
|
|
53
|
+
useLoadMoreInView(list, sentinel, rows.items.length, useCallback(() => fetchNextPage({ cancelRefetch: false }), [fetchNextPage]));
|
|
54
|
+
const optionId = (index) => `${listId}-${index}`;
|
|
55
|
+
const input = useRef(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!open && document.activeElement === input.current) input.current?.select();
|
|
58
|
+
}, [open]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (open && active >= 0) document.getElementById(optionId(active))?.scrollIntoView({ block: "nearest" });
|
|
29
61
|
});
|
|
62
|
+
const close = () => {
|
|
63
|
+
setText("");
|
|
64
|
+
setActive(-1);
|
|
65
|
+
setOpen(false);
|
|
66
|
+
};
|
|
67
|
+
const choose = (item) => {
|
|
68
|
+
onChange(String(item[keyField]), String(item[labelField]));
|
|
69
|
+
close();
|
|
70
|
+
};
|
|
71
|
+
const onKeyDown = (event) => {
|
|
72
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
if (!open) return setOpen(true);
|
|
75
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
76
|
+
setActive((current) => Math.min(Math.max(current + step, 0), items.length - 1));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (event.key === "Enter" && open && items[active]) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
choose(items[active]);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (event.key === "Escape" && open) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
close();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
30
90
|
return /* @__PURE__ */ jsxs("div", {
|
|
31
91
|
className: "relative",
|
|
32
92
|
children: [/* @__PURE__ */ jsx(Input, {
|
|
93
|
+
ref: input,
|
|
94
|
+
role: "combobox",
|
|
95
|
+
"aria-autocomplete": "list",
|
|
96
|
+
"aria-expanded": open,
|
|
97
|
+
"aria-controls": listId,
|
|
98
|
+
"aria-activedescendant": open && items[active] ? optionId(active) : void 0,
|
|
33
99
|
invalid,
|
|
34
|
-
placeholder: `Search ${target.name}`,
|
|
100
|
+
placeholder: `Search ${(view?.names?.plural ?? humanize(target.name)).toLowerCase()}`,
|
|
35
101
|
value: open ? text : label ?? value,
|
|
36
102
|
onFocus: () => setOpen(true),
|
|
37
|
-
onBlur:
|
|
38
|
-
|
|
103
|
+
onBlur: close,
|
|
104
|
+
onKeyDown,
|
|
105
|
+
onChange: (event) => {
|
|
106
|
+
setText(event.target.value);
|
|
107
|
+
setActive(event.target.value.trim() ? 0 : -1);
|
|
108
|
+
setOpen(true);
|
|
109
|
+
}
|
|
39
110
|
}), open && /* @__PURE__ */ jsxs("ul", {
|
|
111
|
+
ref: setList,
|
|
112
|
+
id: listId,
|
|
40
113
|
role: "listbox",
|
|
41
114
|
className: "absolute z-30 mt-1 max-h-60 w-full overflow-y-auto rounded-md border bg-background py-1 shadow-pop",
|
|
115
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
42
116
|
children: [
|
|
43
|
-
|
|
44
|
-
|
|
117
|
+
items.map((item, index) => /* @__PURE__ */ jsx("li", {
|
|
118
|
+
id: optionId(index),
|
|
45
119
|
role: "option",
|
|
46
|
-
"aria-selected":
|
|
47
|
-
className: "
|
|
48
|
-
|
|
49
|
-
onClick: () =>
|
|
50
|
-
onChange(String(item[keyField]), String(item[labelField]));
|
|
51
|
-
setText("");
|
|
52
|
-
setOpen(false);
|
|
53
|
-
},
|
|
120
|
+
"aria-selected": index === active,
|
|
121
|
+
className: cn("cursor-pointer truncate px-3 py-1.5", index === active && "bg-muted", String(item[keyField]) === value && "font-medium"),
|
|
122
|
+
onMouseMove: () => index !== active && setActive(index),
|
|
123
|
+
onClick: () => choose(item),
|
|
54
124
|
children: String(item[labelField])
|
|
55
|
-
}
|
|
56
|
-
|
|
125
|
+
}, String(item[keyField]))),
|
|
126
|
+
more && /* @__PURE__ */ jsx("li", {
|
|
127
|
+
ref: setSentinel,
|
|
128
|
+
"aria-hidden": true,
|
|
129
|
+
className: "h-px"
|
|
130
|
+
}),
|
|
131
|
+
rows.error && !expensive && /* @__PURE__ */ jsx("li", {
|
|
132
|
+
className: "px-3 py-1.5 text-muted-foreground",
|
|
133
|
+
children: "Could not load the options"
|
|
134
|
+
}),
|
|
135
|
+
!more && !loading && !rows.error && items.length === 0 && /* @__PURE__ */ jsx("li", {
|
|
57
136
|
className: "px-3 py-1.5 text-muted-foreground",
|
|
58
137
|
children: "No matches"
|
|
59
138
|
}),
|
|
60
|
-
|
|
139
|
+
loading && /* @__PURE__ */ jsx("li", {
|
|
61
140
|
className: "px-3 py-1.5 text-muted-foreground",
|
|
62
141
|
children: "Loading"
|
|
63
142
|
})
|
|
@@ -22,6 +22,7 @@ export declare const useClient: () => {
|
|
|
22
22
|
limit?: number;
|
|
23
23
|
label?: string;
|
|
24
24
|
}) => Promise<import("@protobase/client").Facet[]>;
|
|
25
|
+
aggregate: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").AggregateParams) => Promise<import("@protobase/client").Aggregate>;
|
|
25
26
|
series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
|
|
26
27
|
filter?: import("@protobase/client").FilterInput;
|
|
27
28
|
}) => Promise<import("@protobase/client").Series>;
|
|
@@ -62,6 +63,7 @@ export declare const useResourceClient: (resource: string) => {
|
|
|
62
63
|
limit?: number;
|
|
63
64
|
label?: string;
|
|
64
65
|
}) => Promise<import("@protobase/client").Facet[]>;
|
|
66
|
+
aggregate: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").AggregateParams) => Promise<import("@protobase/client").Aggregate>;
|
|
65
67
|
series: (resource: import("@protobase/client").ResourceRef, params: import("@protobase/client").SeriesParams & {
|
|
66
68
|
filter?: import("@protobase/client").FilterInput;
|
|
67
69
|
}) => Promise<import("@protobase/client").Series>;
|
package/dist/data/query-keys.js
CHANGED
|
@@ -39,5 +39,18 @@ const useHistogram = (resource, field, filter, buckets = 20, enabled = true) =>
|
|
|
39
39
|
})
|
|
40
40
|
});
|
|
41
41
|
};
|
|
42
|
+
/** Totals per value or date bucket of a field (`:aggregate`), refetched whenever the resource is written. */
|
|
43
|
+
const useAggregate = (resource, params, enabled = true) => {
|
|
44
|
+
const client = useClient();
|
|
45
|
+
const wire = {
|
|
46
|
+
...params,
|
|
47
|
+
filter: filterText(params.filter) ?? ""
|
|
48
|
+
};
|
|
49
|
+
return useQuery({
|
|
50
|
+
queryKey: keys.aggregate(resource, JSON.stringify(wire)),
|
|
51
|
+
enabled,
|
|
52
|
+
queryFn: () => client.aggregate(resource, wire)
|
|
53
|
+
});
|
|
54
|
+
};
|
|
42
55
|
//#endregion
|
|
43
|
-
export { useFacets, useHistogram, useSeries };
|
|
56
|
+
export { useAggregate, useFacets, useHistogram, useSeries };
|
package/dist/index.js
CHANGED
|
@@ -37,14 +37,14 @@ import { Field } from "./record-view/field.js";
|
|
|
37
37
|
import { Section } from "./record-view/section.js";
|
|
38
38
|
import { Textarea } from "./primitives/textarea.js";
|
|
39
39
|
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
40
|
+
import { useList } from "./data/use-list.js";
|
|
40
41
|
import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
|
|
41
42
|
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
43
|
+
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
42
44
|
import { SignInPrompt } from "./auth/sign-in-prompt.js";
|
|
43
45
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
44
46
|
import { MenuSelect } from "./primitives/menu-select.js";
|
|
45
47
|
import { Form } from "./app/create/form.js";
|
|
46
|
-
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
47
|
-
import { useList } from "./data/use-list.js";
|
|
48
48
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
49
49
|
import { App } from "./app/app.js";
|
|
50
50
|
import { RecordLink } from "./app/displays/record-link.js";
|
package/dist/package.js
CHANGED