@protobase/ui 0.12.13 → 0.12.15
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/displays/card-carousel.js +77 -0
- package/dist/app/displays/card-photos.js +21 -0
- package/dist/app/displays/cards-display.js +24 -11
- package/dist/app/displays/list-displays.js +3 -1
- package/dist/data-grid/cells.js +1 -1
- package/dist/format/money.js +11 -5
- package/dist/live/columns-from-model.js +4 -2
- package/dist/live/filter-string.js +35 -15
- package/dist/live/use-filter-config.js +24 -8
- package/dist/live/view-fields.js +21 -8
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BdW9Gl4s.js +65 -0
- package/dist/shell/assets/index-BDDeq8hJ.css +2 -0
- package/dist/shell/assets/{index-Cog2ukQk.js → index-C9fFo4HK.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DzJvhCVz.js → shared-protobase-ui-C_74wCDl.js} +1 -1
- 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-Cn69QiC_.js +0 -65
- package/dist/shell/assets/index-Crnlycqw.css +0 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { keys } from "../../data/query-keys.js";
|
|
3
|
+
import { useClient } from "../../data/api-provider.js";
|
|
4
|
+
import { cardPhotoList, cardPhotosRequest, shownPhoto } from "./card-photos.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { useQuery } from "@tanstack/react-query";
|
|
9
|
+
//#region src/app/displays/card-carousel.tsx
|
|
10
|
+
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-within/card:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 [@media(hover:none)]:opacity-100";
|
|
11
|
+
/**
|
|
12
|
+
* A card whose picture steps through its photos with arrows, the card's own image first. The photos load when an arrow
|
|
13
|
+
* is first pressed, so the list loads as it does without them. The arrows sit over the card's link, beside it in the
|
|
14
|
+
* document, so pressing one does not open the record; the card's list item must be `relative group/card`.
|
|
15
|
+
*/
|
|
16
|
+
const CardCarousel = ({ photos, recordKey, cover, children }) => {
|
|
17
|
+
const client = useClient();
|
|
18
|
+
const [opened, setOpened] = useState(false);
|
|
19
|
+
const [step, setStep] = useState(0);
|
|
20
|
+
const request = cardPhotosRequest(photos, recordKey);
|
|
21
|
+
const loaded = useQuery({
|
|
22
|
+
queryKey: keys.list(photos.resource, request.filter, request.orderBy ?? "", request.pageSize, `card-photos:${photos.image}`),
|
|
23
|
+
enabled: opened,
|
|
24
|
+
queryFn: async () => (await client.list(photos.resource, request)).items.map((item) => item[photos.image])
|
|
25
|
+
});
|
|
26
|
+
const list = cardPhotoList(cover, loaded.data ?? []);
|
|
27
|
+
const index = shownPhoto(step, list.length);
|
|
28
|
+
const press = (by) => {
|
|
29
|
+
setOpened(true);
|
|
30
|
+
setStep((current) => current + by);
|
|
31
|
+
};
|
|
32
|
+
const single = loaded.data !== void 0 && list.length < 2;
|
|
33
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(list[index]), !single && /* @__PURE__ */ jsxs("div", {
|
|
34
|
+
className: "pointer-events-none absolute inset-x-px top-px aspect-[4/3]",
|
|
35
|
+
"aria-busy": loaded.isFetching,
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsx("button", {
|
|
38
|
+
type: "button",
|
|
39
|
+
"aria-label": "Previous photo",
|
|
40
|
+
className: cn(arrow, "left-2"),
|
|
41
|
+
onClick: () => press(-1),
|
|
42
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, {
|
|
43
|
+
className: "size-4",
|
|
44
|
+
"aria-hidden": true
|
|
45
|
+
})
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsx("button", {
|
|
48
|
+
type: "button",
|
|
49
|
+
"aria-label": "Next photo",
|
|
50
|
+
className: cn(arrow, "right-2"),
|
|
51
|
+
onClick: () => press(1),
|
|
52
|
+
children: /* @__PURE__ */ jsx(ChevronRight, {
|
|
53
|
+
className: "size-4",
|
|
54
|
+
"aria-hidden": true
|
|
55
|
+
})
|
|
56
|
+
}),
|
|
57
|
+
loaded.error && /* @__PURE__ */ jsx("span", {
|
|
58
|
+
className: "absolute inset-x-2 top-2 rounded bg-background/85 px-1.5 py-0.5 text-center text-xs text-danger-text",
|
|
59
|
+
children: "Could not load the photos"
|
|
60
|
+
}),
|
|
61
|
+
loaded.data !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
62
|
+
className: "absolute inset-x-0 bottom-2 flex justify-center",
|
|
63
|
+
"aria-live": "polite",
|
|
64
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
65
|
+
className: "sr-only",
|
|
66
|
+
children: `Photo ${index + 1} of ${list.length}`
|
|
67
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
68
|
+
className: "flex items-center gap-1 rounded-full bg-background/70 px-1.5 py-1 text-[11px] tabular-nums leading-none",
|
|
69
|
+
"aria-hidden": true,
|
|
70
|
+
children: list.length > 10 ? `${index + 1} / ${list.length}` : list.map((_, dot) => /* @__PURE__ */ jsx("span", { className: cn("size-1.5 rounded-full", dot === index ? "bg-foreground" : "bg-foreground/35") }, dot))
|
|
71
|
+
})]
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
})] });
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { CardCarousel };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isStoredFile } from "../../live/file-values.js";
|
|
2
|
+
import { createWhere, printFilter } from "@protobase/schema";
|
|
3
|
+
//#region src/app/displays/card-photos.ts
|
|
4
|
+
const where = createWhere();
|
|
5
|
+
/** The list request for a card's photos: the records pointing at the card's, in the display's order, with only their image. */
|
|
6
|
+
const cardPhotosRequest = (photos, recordKey) => ({
|
|
7
|
+
filter: printFilter(where.eq(photos.field, recordKey)),
|
|
8
|
+
...photos.sort !== void 0 && { orderBy: photos.sort },
|
|
9
|
+
fields: [photos.image],
|
|
10
|
+
pageSize: 100
|
|
11
|
+
});
|
|
12
|
+
/** The files a card steps through: its own image first, then the loaded photos less that same file; empty values are left out. */
|
|
13
|
+
const cardPhotoList = (cover, loaded) => {
|
|
14
|
+
const coverUri = isStoredFile(cover) ? cover.uri : void 0;
|
|
15
|
+
const rest = loaded.filter((photo) => isStoredFile(photo) && photo.uri !== coverUri);
|
|
16
|
+
return coverUri === void 0 ? rest : [cover, ...rest];
|
|
17
|
+
};
|
|
18
|
+
/** The position shown after `step` presses forward, or back when negative, wrapping around both ends. */
|
|
19
|
+
const shownPhoto = (step, count) => count === 0 ? 0 : (step % count + count) % count;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { cardPhotoList, cardPhotosRequest, shownPhoto };
|
|
@@ -2,7 +2,7 @@ import { Badge } from "../../primitives/badge.js";
|
|
|
2
2
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
4
|
import { useProjectUi } from "../pages/project-ui.js";
|
|
5
|
-
import { cardFields } from "./list-displays.js";
|
|
5
|
+
import { cardFields, filledMeta } from "./list-displays.js";
|
|
6
6
|
import { AppLink } from "../use-app-address.js";
|
|
7
7
|
import { fileShown, isPreviewable } from "../../live/file-values.js";
|
|
8
8
|
import { displayValue } from "../../live/display-value.js";
|
|
@@ -11,6 +11,7 @@ import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
|
11
11
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
12
12
|
import { namedIcon } from "../named-icon.js";
|
|
13
13
|
import { ValueBadge } from "../value-badge.js";
|
|
14
|
+
import { CardCarousel } from "./card-carousel.js";
|
|
14
15
|
import { DisplayFooter, DisplayState } from "./display-frame.js";
|
|
15
16
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
16
17
|
//#region src/app/displays/cards-display.tsx
|
|
@@ -39,9 +40,10 @@ const MetaValue = ({ icon, children }) => {
|
|
|
39
40
|
});
|
|
40
41
|
};
|
|
41
42
|
/**
|
|
42
|
-
* A card per record, opening its page: its image across the top,
|
|
43
|
-
*
|
|
44
|
-
* row, the built-in content again when it is
|
|
43
|
+
* A card per record, opening its page: its image across the top, with arrows through its photos when the display names
|
|
44
|
+
* them, the title over the subtitle, the badge beside it, small meta fields below, those with a value, and the labels of
|
|
45
|
+
* its flags that are true; or, with a `card` component, what it draws of the row, the built-in content again when it is
|
|
46
|
+
* missing or throws.
|
|
45
47
|
*/
|
|
46
48
|
const CardsDisplay = ({ model, view, display, list }) => {
|
|
47
49
|
const { image, title, subtitle, badge, meta, flags } = cardFields(model, view, display);
|
|
@@ -76,6 +78,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
76
78
|
});
|
|
77
79
|
};
|
|
78
80
|
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
81
|
+
const photos = Card ? void 0 : display.photos;
|
|
79
82
|
return /* @__PURE__ */ jsxs(DisplayState, {
|
|
80
83
|
list,
|
|
81
84
|
empty: `No ${plural} match.`,
|
|
@@ -89,8 +92,9 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
89
92
|
children: list.rows.map((row) => {
|
|
90
93
|
const status = badge === void 0 ? void 0 : row.record[badge];
|
|
91
94
|
const raised = flags.filter((name) => row.record[name] === true);
|
|
92
|
-
const
|
|
93
|
-
|
|
95
|
+
const cover = image === void 0 ? void 0 : row.record[image];
|
|
96
|
+
const builtIn = (photo) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
97
|
+
(image !== void 0 || photos !== void 0) && /* @__PURE__ */ jsx(CardImage, { value: photo }),
|
|
94
98
|
/* @__PURE__ */ jsxs("span", {
|
|
95
99
|
className: "flex items-start justify-between gap-2",
|
|
96
100
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -108,7 +112,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
108
112
|
}),
|
|
109
113
|
meta.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
110
114
|
className: "mt-auto flex flex-wrap items-center gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
111
|
-
children: meta.map((name) => /* @__PURE__ */ jsxs("div", {
|
|
115
|
+
children: filledMeta(row, meta).map((name) => /* @__PURE__ */ jsxs("div", {
|
|
112
116
|
className: "flex gap-1",
|
|
113
117
|
children: [/* @__PURE__ */ jsx("dt", {
|
|
114
118
|
className: "sr-only",
|
|
@@ -128,7 +132,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
128
132
|
}) }, name))
|
|
129
133
|
})
|
|
130
134
|
] });
|
|
131
|
-
|
|
135
|
+
const card = (photo) => /* @__PURE__ */ jsx(AppLink, {
|
|
132
136
|
to: {
|
|
133
137
|
kind: "record",
|
|
134
138
|
resource: model.name,
|
|
@@ -137,10 +141,19 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
137
141
|
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",
|
|
138
142
|
children: Card ? /* @__PURE__ */ jsx(CustomBoundary, {
|
|
139
143
|
name: display.card,
|
|
140
|
-
fallback: builtIn,
|
|
144
|
+
fallback: builtIn(photo),
|
|
141
145
|
children: /* @__PURE__ */ jsx(Card, { row })
|
|
142
|
-
}) : builtIn
|
|
143
|
-
})
|
|
146
|
+
}) : builtIn(photo)
|
|
147
|
+
});
|
|
148
|
+
return /* @__PURE__ */ jsx("li", {
|
|
149
|
+
className: photos ? "group/card relative" : void 0,
|
|
150
|
+
children: photos ? /* @__PURE__ */ jsx(CardCarousel, {
|
|
151
|
+
photos,
|
|
152
|
+
recordKey: row.key,
|
|
153
|
+
cover,
|
|
154
|
+
children: card
|
|
155
|
+
}) : card(cover)
|
|
156
|
+
}, row.key);
|
|
144
157
|
})
|
|
145
158
|
}), /* @__PURE__ */ jsx(DisplayFooter, { list })]
|
|
146
159
|
});
|
|
@@ -37,6 +37,8 @@ const cardFields = (model, view, display) => ({
|
|
|
37
37
|
meta: display.meta ?? [],
|
|
38
38
|
flags: display.flags ?? []
|
|
39
39
|
});
|
|
40
|
+
/** The meta fields a card shows for a row: those with a value, so a listing for rent only shows no purchase price. */
|
|
41
|
+
const filledMeta = (row, meta) => meta.filter((name) => row.record[name] !== null && row.record[name] !== void 0 && row.record[name] !== "");
|
|
40
42
|
/** Every field a display shows, the fields its rows are loaded and labelled for. */
|
|
41
43
|
const shownFields = (model, view, display) => {
|
|
42
44
|
if (display.kind === "custom") return display.fields;
|
|
@@ -73,4 +75,4 @@ const displayRow = (model, row) => {
|
|
|
73
75
|
};
|
|
74
76
|
};
|
|
75
77
|
//#endregion
|
|
76
|
-
export { cardFields, defaultDisplay, displayChoices, displayRow, displaysOf, loadedFields, shownDisplay, shownFields, sortFields };
|
|
78
|
+
export { cardFields, defaultDisplay, displayChoices, displayRow, displaysOf, filledMeta, loadedFields, shownDisplay, shownFields, sortFields };
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -29,7 +29,7 @@ const renderCell = (spec, value) => {
|
|
|
29
29
|
});
|
|
30
30
|
case "money": return /* @__PURE__ */ jsxs("span", {
|
|
31
31
|
className: "tabular-nums",
|
|
32
|
-
children: [formatMoney(Number(value), spec.currency), spec.suffix]
|
|
32
|
+
children: [formatMoney(Number(value), spec.currency, spec.decimals), spec.suffix]
|
|
33
33
|
});
|
|
34
34
|
case "date": return /* @__PURE__ */ jsx("span", {
|
|
35
35
|
className: "tabular-nums text-muted-foreground",
|
package/dist/format/money.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
//#region src/format/money.ts
|
|
2
2
|
const formatters = /* @__PURE__ */ new Map();
|
|
3
|
-
const formatterFor = (currency) => {
|
|
4
|
-
const
|
|
3
|
+
const formatterFor = (currency, decimals) => {
|
|
4
|
+
const key = `${currency}:${decimals ?? ""}`;
|
|
5
|
+
const cached = formatters.get(key);
|
|
5
6
|
if (cached) return cached;
|
|
6
7
|
const created = new Intl.NumberFormat("en-IE", {
|
|
7
8
|
style: "currency",
|
|
8
|
-
currency
|
|
9
|
+
currency,
|
|
10
|
+
...decimals !== void 0 && {
|
|
11
|
+
minimumFractionDigits: decimals,
|
|
12
|
+
maximumFractionDigits: decimals
|
|
13
|
+
}
|
|
9
14
|
});
|
|
10
|
-
formatters.set(
|
|
15
|
+
formatters.set(key, created);
|
|
11
16
|
return created;
|
|
12
17
|
};
|
|
13
|
-
|
|
18
|
+
/** An amount in cents in the currency, with its usual decimals or `decimals` of them. */
|
|
19
|
+
const formatMoney = (cents, currency = "EUR", decimals) => formatterFor(currency, decimals).format(cents / 100);
|
|
14
20
|
//#endregion
|
|
15
21
|
export { formatMoney };
|
|
@@ -69,13 +69,14 @@ const columnsFromModel = (resource, view, highlight = []) => {
|
|
|
69
69
|
const viewField = view?.fields[name];
|
|
70
70
|
const kind = kindOf(field, viewField, index);
|
|
71
71
|
const labels = Object.fromEntries((field.enumValues ?? []).map((value) => [value, enumLabel(value, viewField?.valueLabels)]));
|
|
72
|
+
const suffixWidth = viewField?.suffix ? Math.ceil(viewField.suffix.length * 7.2) : 0;
|
|
72
73
|
return [{
|
|
73
74
|
id: name,
|
|
74
75
|
header: viewField?.label ?? humanize(name),
|
|
75
76
|
kind,
|
|
76
|
-
width: name === "name" ? 260 : widths[kind],
|
|
77
|
+
width: (name === "name" ? 260 : widths[kind]) + suffixWidth,
|
|
77
78
|
sortable: field.sortable,
|
|
78
|
-
minWidth: Math.max(kind === "id" ? 150 : 0, headerWidth(viewField?.label ?? humanize(name), field.sortable), kind === "status" ? badgeWidth(Object.values(labels)) : 0),
|
|
79
|
+
minWidth: Math.max(kind === "id" ? 150 : 0, headerWidth(viewField?.label ?? humanize(name), field.sortable), kind === "status" ? badgeWidth(Object.values(labels)) : 0, suffixWidth > 0 ? widths[kind] + suffixWidth : 0),
|
|
79
80
|
...fixedKinds.has(kind) && { fixed: true },
|
|
80
81
|
...highlight.includes(name) && { highlight: true },
|
|
81
82
|
...field.type === "enum" && {
|
|
@@ -85,6 +86,7 @@ const columnsFromModel = (resource, view, highlight = []) => {
|
|
|
85
86
|
...field.type === "enum" && viewField?.valueColors && { colors: viewField.valueColors },
|
|
86
87
|
...field.type === "enum" && viewField?.badgeShape && { shape: viewField.badgeShape },
|
|
87
88
|
...viewField?.suffix !== void 0 && { suffix: viewField.suffix },
|
|
89
|
+
...kind === "money" && viewField?.decimals !== void 0 && { decimals: viewField.decimals },
|
|
88
90
|
...kind === "money" && {
|
|
89
91
|
currency: "EUR",
|
|
90
92
|
value: (row) => row[name] == null ? null : currencyCents(row[name])
|
|
@@ -60,6 +60,10 @@ const stateToClauses = (config, state, now = /* @__PURE__ */ new Date()) => {
|
|
|
60
60
|
const facets = config.facets.flatMap((group) => {
|
|
61
61
|
const values = state.facets[group.id] ?? [];
|
|
62
62
|
if (values.length === 0) return [];
|
|
63
|
+
if (group.flags) {
|
|
64
|
+
const chosen = group.options.filter((option) => values.includes(option.value)).map((option) => w.eq(option.value, true));
|
|
65
|
+
return chosen.length === 0 ? [] : [chosen.length === 1 ? chosen[0] : w.or(...chosen)];
|
|
66
|
+
}
|
|
63
67
|
return [values.length === 1 ? w.eq(group.id, values[0]) : w.in(group.id, values)];
|
|
64
68
|
});
|
|
65
69
|
const ranges = (config.ranges ?? []).flatMap((range) => {
|
|
@@ -79,6 +83,17 @@ const clausesToText = (clauses) => clauses.length === 0 ? "" : printFilter(w.and
|
|
|
79
83
|
const stateToText = (config, state, extras = [], now = /* @__PURE__ */ new Date()) => clausesToText([...stateToClauses(config, state, now), ...extras]);
|
|
80
84
|
const flatten = (expr) => expr.kind === "and" ? expr.args.flatMap(flatten) : [expr];
|
|
81
85
|
const fieldName = (expr) => expr.field.path.join(".");
|
|
86
|
+
const isTrue = (expr) => expr.kind === "compare" && expr.op === "=" && expr.value.kind === "boolean" && expr.value.value;
|
|
87
|
+
const flagsChosen = (clause, groupOf) => {
|
|
88
|
+
const parts = clause.kind === "or" ? clause.args : [clause];
|
|
89
|
+
if (!parts.every(isTrue)) return void 0;
|
|
90
|
+
const fields = parts.map(fieldName);
|
|
91
|
+
const group = groupOf.get(fields[0]);
|
|
92
|
+
return group !== void 0 && fields.every((field) => groupOf.get(field) === group) ? {
|
|
93
|
+
group,
|
|
94
|
+
fields
|
|
95
|
+
} : void 0;
|
|
96
|
+
};
|
|
82
97
|
const literalText = (value) => value.kind === "number" ? String(value.raw) : value.kind === "string" ? String(value.value) : void 0;
|
|
83
98
|
const presetOf = (clause, now) => {
|
|
84
99
|
const v = clause.value;
|
|
@@ -104,26 +119,31 @@ const textToState = (config, text, now = /* @__PURE__ */ new Date()) => {
|
|
|
104
119
|
toggles: []
|
|
105
120
|
};
|
|
106
121
|
const extras = [];
|
|
107
|
-
const facetIds = new Set(config.facets.map((group) => group.id));
|
|
122
|
+
const facetIds = new Set(config.facets.filter((group) => !group.flags).map((group) => group.id));
|
|
123
|
+
const flagGroupOf = new Map(config.facets.filter((group) => group.flags).flatMap((group) => group.options.map((option) => [option.value, group.id])));
|
|
108
124
|
const toggleIds = new Set((config.toggles ?? []).map((toggle) => toggle.id));
|
|
109
125
|
const rangeIds = new Set((config.ranges ?? []).map((range) => range.id));
|
|
110
126
|
const bounds = /* @__PURE__ */ new Map();
|
|
111
127
|
let dateClause;
|
|
112
128
|
let todayEnd;
|
|
113
|
-
for (const clause of parsed.ast ? flatten(parsed.ast) : [])
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
state.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (clause.
|
|
123
|
-
else
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
for (const clause of parsed.ast ? flatten(parsed.ast) : []) {
|
|
130
|
+
const flags = flagsChosen(clause, flagGroupOf);
|
|
131
|
+
if (flags && !state.facets[flags.group]) state.facets[flags.group] = flags.fields;
|
|
132
|
+
else if (clause.kind === "compare" && facetIds.has(fieldName(clause)) && clause.op === "=" && literalText(clause.value) !== void 0) state.facets[fieldName(clause)] = [...state.facets[fieldName(clause)] ?? [], literalText(clause.value)];
|
|
133
|
+
else if (clause.kind === "in" && facetIds.has(fieldName(clause)) && clause.values.every((value) => literalText(value) !== void 0)) state.facets[fieldName(clause)] = [...state.facets[fieldName(clause)] ?? [], ...clause.values.map((value) => literalText(value))];
|
|
134
|
+
else if (clause.kind === "compare" && config.dateField === fieldName(clause) && clause.op === ">=" && presetOf(clause, now) && !state.date) {
|
|
135
|
+
state.date = presetOf(clause, now);
|
|
136
|
+
dateClause = clause;
|
|
137
|
+
} else if (clause.kind === "compare" && config.dateField === fieldName(clause) && clause.op === "<" && (clause.value.kind === "timestamp" || clause.value.kind === "string") && periodStart("today", now).startsWith(clause.value.value.slice(0, 10))) todayEnd = clause;
|
|
138
|
+
else if (clause.kind === "compare" && toggleIds.has(fieldName(clause)) && clause.op === "=" && clause.value.kind === "boolean" && clause.value.value) state.toggles.push(fieldName(clause));
|
|
139
|
+
else if (clause.kind === "compare" && rangeIds.has(fieldName(clause)) && (clause.op === ">=" || clause.op === "<=") && clause.value.kind === "number") {
|
|
140
|
+
const range = bounds.get(fieldName(clause)) ?? { clauses: [] };
|
|
141
|
+
if (clause.op === ">=") range.low = clause.value.value;
|
|
142
|
+
else range.high = clause.value.value;
|
|
143
|
+
range.clauses.push(clause);
|
|
144
|
+
bounds.set(fieldName(clause), range);
|
|
145
|
+
} else extras.push(clause);
|
|
146
|
+
}
|
|
127
147
|
if (state.date === "yesterday" && !todayEnd) {
|
|
128
148
|
extras.push(dateClause);
|
|
129
149
|
state.date = void 0;
|
|
@@ -25,25 +25,30 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
25
25
|
const field = model.fields[name];
|
|
26
26
|
return field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
|
|
27
27
|
};
|
|
28
|
-
const
|
|
28
|
+
const counted = skeleton.facets.flatMap((group) => (group.flags ? group.options.map((option) => option.value) : [group.id]).map((field) => ({
|
|
29
|
+
group: group.id,
|
|
30
|
+
field
|
|
31
|
+
})));
|
|
32
|
+
const facetQueries = useQueries({ queries: counted.map(({ group, field }) => {
|
|
29
33
|
const filter = stateToText(skeleton, {
|
|
30
34
|
...state,
|
|
31
35
|
facets: {
|
|
32
36
|
...state.facets,
|
|
33
|
-
[group
|
|
37
|
+
[group]: []
|
|
34
38
|
}
|
|
35
39
|
}, extras);
|
|
36
40
|
return {
|
|
37
|
-
queryKey: keys.facets(model.name,
|
|
38
|
-
queryFn: () => client.facets(model.name,
|
|
41
|
+
queryKey: keys.facets(model.name, field, filter),
|
|
42
|
+
queryFn: () => client.facets(model.name, field, {
|
|
39
43
|
filter,
|
|
40
44
|
limit: 50
|
|
41
45
|
})
|
|
42
46
|
};
|
|
43
47
|
}) });
|
|
44
|
-
const
|
|
48
|
+
const counts = (group, field) => facetQueries[counted.findIndex((entry) => entry.group === group && entry.field === field)]?.data;
|
|
49
|
+
const labelQueries = useQueries({ queries: skeleton.facets.map((group) => {
|
|
45
50
|
const target = targetOf(group.id);
|
|
46
|
-
const ids = labelKeys((
|
|
51
|
+
const ids = labelKeys((counts(group.id, group.id) ?? []).flatMap((facet) => facet.value === null ? [] : [String(facet.value)]), state.facets[group.id] ?? []);
|
|
47
52
|
return {
|
|
48
53
|
queryKey: keys.labels(`facet:${model.name}.${group.id}`, ids.join(",")),
|
|
49
54
|
enabled: Boolean(target) && ids.length > 0,
|
|
@@ -67,9 +72,20 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
67
72
|
};
|
|
68
73
|
}) });
|
|
69
74
|
const facetGroup = (group, index) => {
|
|
75
|
+
if (group.flags) {
|
|
76
|
+
const options = group.options.map(({ value }) => ({
|
|
77
|
+
value,
|
|
78
|
+
label: label(value),
|
|
79
|
+
count: counts(group.id, value)?.find((facet) => String(facet.value) === "true")?.count ?? 0
|
|
80
|
+
}));
|
|
81
|
+
return {
|
|
82
|
+
...group,
|
|
83
|
+
options
|
|
84
|
+
};
|
|
85
|
+
}
|
|
70
86
|
const field = model.fields[group.id];
|
|
71
87
|
const names = new Map(labelQueries[index]?.data ?? []);
|
|
72
|
-
const
|
|
88
|
+
const values = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.value === null ? [] : [{
|
|
73
89
|
value: String(facet.value),
|
|
74
90
|
label: field?.type === "enum" ? enumLabel(String(facet.value), view?.fields[group.id]?.valueLabels) : names.get(String(facet.value)) ?? String(facet.value),
|
|
75
91
|
count: facet.count
|
|
@@ -78,7 +94,7 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
78
94
|
return {
|
|
79
95
|
...group,
|
|
80
96
|
label: label(group.id),
|
|
81
|
-
options: search.matches ? matchOptions(search.matches,
|
|
97
|
+
options: search.matches ? matchOptions(search.matches, values) : values,
|
|
82
98
|
...names.size > 0 && { labels: Object.fromEntries(names) },
|
|
83
99
|
...search.onSearch && { onSearch: search.onSearch }
|
|
84
100
|
};
|
package/dist/live/view-fields.js
CHANGED
|
@@ -27,14 +27,27 @@ const widgetFields = (view, model) => {
|
|
|
27
27
|
const widgets = view?.filters ?? [];
|
|
28
28
|
const date = widgets.find((widget) => widget.kind === "dateRange");
|
|
29
29
|
return {
|
|
30
|
-
facets: widgets.flatMap((widget) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
facets: widgets.flatMap((widget) => {
|
|
31
|
+
if (widget.kind === "facets" && !rangeOf(widget, model)) return [{
|
|
32
|
+
id: widget.field,
|
|
33
|
+
label: widget.field,
|
|
34
|
+
options: [],
|
|
35
|
+
searchable: widget.search,
|
|
36
|
+
...model?.fields[widget.field]?.type === "relation" && { combobox: true }
|
|
37
|
+
}];
|
|
38
|
+
if (widget.kind === "flags") return [{
|
|
39
|
+
id: widget.fields.join("|"),
|
|
40
|
+
label: widget.label,
|
|
41
|
+
options: widget.fields.map((field) => ({
|
|
42
|
+
value: field,
|
|
43
|
+
label: field,
|
|
44
|
+
count: 0
|
|
45
|
+
})),
|
|
46
|
+
flags: true
|
|
47
|
+
}];
|
|
48
|
+
return [];
|
|
49
|
+
}),
|
|
50
|
+
ranges: widgets.flatMap((widget) => widget.kind !== "flags" && rangeOf(widget, model) ? [emptyRange(widget.field)] : []),
|
|
38
51
|
...date && {
|
|
39
52
|
dateField: date.field,
|
|
40
53
|
dateKind: model?.fields[date.field]?.type === "date" ? "date" : "timestamp",
|
package/dist/package.js
CHANGED