@protobase/ui 0.55.1 → 0.55.3
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/app.js +1 -1
- package/dist/app/displays/record-card.js +8 -0
- package/dist/app/embedded-list/embedded-columns.js +2 -1
- package/dist/app/embedded-list/use-embedded-rows.js +6 -2
- package/dist/app/list-page.js +6 -2
- package/dist/app/nav-from-meta.js +19 -3
- package/dist/app/pages/field-value.js +3 -14
- package/dist/app/record-page.js +1 -1
- package/dist/app/related-cards/related-card.js +1 -1
- package/dist/app/relation-picker.js +12 -4
- package/dist/app/titles/record-titles.js +1 -1
- package/dist/app/titles/relation-title-cells.js +28 -0
- package/dist/app/titles/relation-title-text.js +7 -0
- package/dist/app/titles/relation-title.js +3 -11
- package/dist/app/titles/use-relation-title.js +25 -0
- package/dist/app/use-related-records.js +1 -1
- package/dist/app-shell/sidebar-create-link.js +26 -0
- package/dist/app-shell/sidebar-group.js +6 -14
- package/dist/app-shell/sidebar-rows.js +13 -0
- package/dist/app-shell/sidebar.js +11 -5
- package/dist/index.js +2 -2
- package/dist/live/grid-loader.js +5 -3
- package/dist/live/relation-labels.js +5 -2
- package/dist/live/title-rows.js +47 -0
- package/dist/live/use-filter-config.js +27 -6
- package/dist/live/use-reference-search.js +14 -7
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-CAj-S5YI.js +9 -0
- package/dist/shell/assets/{index-BX1N2n2W.css → index-BRYcNA4H.css} +1 -1
- package/dist/shell/assets/{index-DprdVbQM.js → index-CEhwvXIf.js} +2 -2
- package/dist/shell/assets/{records-map-DhDIjikh.js → records-map-Cp4sUeC-.js} +2 -2
- package/dist/shell/assets/{shared-protobase-ui-uXIvgyTZ.js → shared-protobase-ui-Dd4pdw8C.js} +2 -2
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-CAj-S5YI.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-DprdVbQM.js.map → index-CEhwvXIf.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-DhDIjikh.js.map → records-map-Cp4sUeC-.js.map} +1 -1
- package/dist/shell-source-maps/assets/{shared-protobase-ui-uXIvgyTZ.js.map → shared-protobase-ui-Dd4pdw8C.js.map} +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-DxuzNqzR.js +0 -9
- package/dist/shell-source-maps/assets/app-DxuzNqzR.js.map +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { keys } from "../data/query-keys.js";
|
|
2
|
+
import { createWhere, printFilter } from "@protobase/schema";
|
|
3
|
+
//#region src/live/title-rows.ts
|
|
4
|
+
const where$1 = createWhere();
|
|
5
|
+
/** The fields a derived title reads, when a single-key `target`'s view derives its titles from fields the reader may read. */
|
|
6
|
+
const derivedTitleFields = (target, view) => {
|
|
7
|
+
const from = view?.titleFrom;
|
|
8
|
+
if (!from || from.unreadable || target.primaryKey.length !== 1) return;
|
|
9
|
+
return from.fields;
|
|
10
|
+
};
|
|
11
|
+
/** Where the fields a record's derived title reads are cached, `{ row }` with `row` null for a record not found. */
|
|
12
|
+
const titleRowKey = (target, id, fields) => keys.titleRow(target.name, id, fields.join(","));
|
|
13
|
+
/** The key and `fields` of the records `ids` of `target`, in one list call, by key. */
|
|
14
|
+
const readTitleRows = async (client, target, fields, ids) => {
|
|
15
|
+
const keyField = target.primaryKey[0];
|
|
16
|
+
const unique = [...new Set(ids)];
|
|
17
|
+
if (unique.length === 0) return /* @__PURE__ */ new Map();
|
|
18
|
+
const page = await client.list(target.name, {
|
|
19
|
+
filter: printFilter(where$1.in(keyField, unique)),
|
|
20
|
+
fields: [keyField, ...fields],
|
|
21
|
+
pageSize: unique.length
|
|
22
|
+
});
|
|
23
|
+
return new Map(page.items.map((item) => [String(item[keyField]), item]));
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reads, for one page of `rows`, the fields the derived titles of the records their relations among `columns` point at
|
|
27
|
+
* read: one list call per target resource, each record cached under its `titleRowKey`, so the cells titling them for
|
|
28
|
+
* the reader ask nothing more.
|
|
29
|
+
*/
|
|
30
|
+
const seedTitleRows = async (queryClient, client, resources, views, model, columns, rows) => {
|
|
31
|
+
const ids = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const name of columns) {
|
|
33
|
+
const field = model.fields[name];
|
|
34
|
+
const target = field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
|
|
35
|
+
if (!target || !derivedTitleFields(target, views[target.name])) continue;
|
|
36
|
+
const values = rows.flatMap((row) => row[name] == null ? [] : [String(row[name])]);
|
|
37
|
+
ids.set(target.name, [...ids.get(target.name) ?? [], ...values]);
|
|
38
|
+
}
|
|
39
|
+
await Promise.all([...ids].map(async ([name, wanted]) => {
|
|
40
|
+
const target = resources[name];
|
|
41
|
+
const fields = derivedTitleFields(target, views[name]);
|
|
42
|
+
const found = await readTitleRows(client, target, fields, wanted);
|
|
43
|
+
for (const id of new Set(wanted)) queryClient.setQueryData(titleRowKey(target, id, fields), { row: found.get(id) ?? null });
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { derivedTitleFields, readTitleRows, seedTitleRows, titleRowKey };
|
|
@@ -3,6 +3,7 @@ import { useClient } from "../data/api-provider.js";
|
|
|
3
3
|
import { humanize } from "./naming.js";
|
|
4
4
|
import { labelFieldOf } from "./model-helpers.js";
|
|
5
5
|
import { fetchLabels } from "./relation-labels.js";
|
|
6
|
+
import { derivedTitleFields, readTitleRows } from "./title-rows.js";
|
|
6
7
|
import { setRange } from "../filter-panel/filter-state.js";
|
|
7
8
|
import { stateToText } from "./filter-string.js";
|
|
8
9
|
import { rangeFormat, rangeHistogram, widgetFields } from "./view-fields.js";
|
|
@@ -16,7 +17,7 @@ const histogramBuckets = 24;
|
|
|
16
17
|
* The panel's config for a view: facet counts come from `:facets` and each range's bounds from `:histogram`, each
|
|
17
18
|
* computed with every other active filter applied but not its own, so choosing one value does not hide the rest.
|
|
18
19
|
*/
|
|
19
|
-
const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
20
|
+
const useFilterConfig = ({ model, resources, views, view, state, extras, titleOf }) => {
|
|
20
21
|
const client = useClient();
|
|
21
22
|
const queryClient = useQueryClient();
|
|
22
23
|
const skeleton = widgetFields(view, model);
|
|
@@ -25,6 +26,7 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
25
26
|
const field = model.fields[name];
|
|
26
27
|
return field?.type === "relation" && field.relation ? resources[field.relation.resource] : void 0;
|
|
27
28
|
};
|
|
29
|
+
const derives = (target) => Boolean(target && views[target.name]?.titleFrom);
|
|
28
30
|
const counted = skeleton.facets.flatMap((group) => (group.flags ? group.options.map((option) => option.value) : [group.id]).map((field) => ({
|
|
29
31
|
group: group.id,
|
|
30
32
|
field
|
|
@@ -38,7 +40,7 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
38
40
|
}
|
|
39
41
|
}, extras);
|
|
40
42
|
const target = targetOf(field);
|
|
41
|
-
const label = target?.primaryKey.length === 1 ? labelFieldOf(target, views[target.name]) : void 0;
|
|
43
|
+
const label = target?.primaryKey.length === 1 && !derives(target) ? labelFieldOf(target, views[target.name]) : void 0;
|
|
42
44
|
return {
|
|
43
45
|
queryKey: keys.facets(model.name, field, filter),
|
|
44
46
|
queryFn: () => client.facets(model.name, field, {
|
|
@@ -49,19 +51,34 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
49
51
|
};
|
|
50
52
|
}) });
|
|
51
53
|
const counts = (group, field) => facetQueries[counted.findIndex((entry) => entry.group === group && entry.field === field)]?.data;
|
|
54
|
+
const countedKeys = (group) => (counts(group, group) ?? []).flatMap((facet) => facet.value === null ? [] : [String(facet.value)]);
|
|
52
55
|
const labelQueries = useQueries({ queries: skeleton.facets.map((group) => {
|
|
53
56
|
const target = targetOf(group.id);
|
|
54
|
-
const ids = labelKeys((
|
|
57
|
+
const ids = labelKeys(countedKeys(group.id), state.facets[group.id] ?? []);
|
|
55
58
|
return {
|
|
56
59
|
queryKey: keys.labels(`facet:${model.name}.${group.id}`, ids.join(",")),
|
|
57
|
-
enabled: Boolean(target) && ids.length > 0,
|
|
60
|
+
enabled: Boolean(target) && !derives(target) && ids.length > 0,
|
|
58
61
|
queryFn: async () => [...await fetchLabels(queryClient, client, target, ids, views[target.name])]
|
|
59
62
|
};
|
|
60
63
|
}) });
|
|
64
|
+
const titleQueries = useQueries({ queries: skeleton.facets.map((group) => {
|
|
65
|
+
const target = targetOf(group.id);
|
|
66
|
+
const fields = target && derives(target) ? derivedTitleFields(target, views[target.name]) : void 0;
|
|
67
|
+
const ids = [.../* @__PURE__ */ new Set([...countedKeys(group.id), ...state.facets[group.id] ?? []])];
|
|
68
|
+
return {
|
|
69
|
+
queryKey: keys.labels(`facet-title:${model.name}.${group.id}`, ids.join(",")),
|
|
70
|
+
enabled: fields !== void 0 && ids.length > 0,
|
|
71
|
+
queryFn: async () => [...await readTitleRows(client, target, fields, ids)]
|
|
72
|
+
};
|
|
73
|
+
}) });
|
|
74
|
+
const titled = (group, index) => {
|
|
75
|
+
const target = targetOf(group);
|
|
76
|
+
return (titleQueries[index]?.data ?? []).map(([id, row]) => [id, titleOf(target, views[target.name], row, id)]);
|
|
77
|
+
};
|
|
61
78
|
const searches = useReferenceSearch(skeleton.facets.map((group) => ({
|
|
62
79
|
id: group.id,
|
|
63
80
|
target: group.combobox ? targetOf(group.id) : void 0
|
|
64
|
-
})), views);
|
|
81
|
+
})), views, titleOf);
|
|
65
82
|
const ranges = skeleton.ranges ?? [];
|
|
66
83
|
const histogramQueries = useQueries({ queries: ranges.map((range) => {
|
|
67
84
|
const filter = stateToText(skeleton, setRange(state, range.id, void 0), extras);
|
|
@@ -88,7 +105,11 @@ const useFilterConfig = ({ model, resources, views, view, state, extras }) => {
|
|
|
88
105
|
}
|
|
89
106
|
const field = model.fields[group.id];
|
|
90
107
|
const named = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.label === void 0 ? [] : [[String(facet.value), facet.label]]);
|
|
91
|
-
const names = new Map([
|
|
108
|
+
const names = new Map([
|
|
109
|
+
...named,
|
|
110
|
+
...labelQueries[index]?.data ?? [],
|
|
111
|
+
...titled(group.id, index)
|
|
112
|
+
]);
|
|
92
113
|
const values = (counts(group.id, group.id) ?? []).flatMap((facet) => facet.value === null ? [] : [{
|
|
93
114
|
value: String(facet.value),
|
|
94
115
|
label: field?.type === "enum" ? enumLabel(String(facet.value), view?.fields[group.id]?.valueLabels) : names.get(String(facet.value)) ?? String(facet.value),
|
|
@@ -8,12 +8,21 @@ const where$1 = createWhere();
|
|
|
8
8
|
/**
|
|
9
9
|
* The search text typed into each facet's combobox and the records of its target resource that match it. A facet
|
|
10
10
|
* without a target, or whose target has no search fields or a composite key, gets nothing: its options are filtered
|
|
11
|
-
* by name instead.
|
|
11
|
+
* by name instead. A target whose view derives its titles names its records by `titleOf`, for the reader.
|
|
12
12
|
*/
|
|
13
|
-
const useReferenceSearch = (targets, views) => {
|
|
13
|
+
const useReferenceSearch = (targets, views, titleOf) => {
|
|
14
14
|
const client = useClient();
|
|
15
15
|
const [texts, setTexts] = useState({});
|
|
16
16
|
const searchable = (target) => Boolean(target?.search?.length) && target.primaryKey.length === 1;
|
|
17
|
+
const nameFields = (target) => {
|
|
18
|
+
const view = views[target.name];
|
|
19
|
+
return [target.primaryKey[0], ...view?.titleFrom ? view.titleFrom.fields : [labelFieldOf(target, view)]];
|
|
20
|
+
};
|
|
21
|
+
const named = (target, item) => {
|
|
22
|
+
const view = views[target.name];
|
|
23
|
+
const key = String(item[target.primaryKey[0]]);
|
|
24
|
+
return [key, view?.titleFrom ? titleOf(target, view, item, key) : String(item[labelFieldOf(target, view)])];
|
|
25
|
+
};
|
|
17
26
|
const results = useQueries({ queries: targets.map(({ id, target }) => {
|
|
18
27
|
const text = (texts[id] ?? "").trim();
|
|
19
28
|
return {
|
|
@@ -26,13 +35,11 @@ const useReferenceSearch = (targets, views) => {
|
|
|
26
35
|
enabled: searchable(target) && text !== "",
|
|
27
36
|
placeholderData: keepPreviousData,
|
|
28
37
|
queryFn: async () => {
|
|
29
|
-
const keyField = target.primaryKey[0];
|
|
30
|
-
const labelField = labelFieldOf(target, views[target.name]);
|
|
31
38
|
return (await client.list(target.name, {
|
|
32
39
|
filter: printFilter(where$1.search(text)),
|
|
33
|
-
fields:
|
|
40
|
+
fields: nameFields(target),
|
|
34
41
|
pageSize: 20
|
|
35
|
-
})).items
|
|
42
|
+
})).items;
|
|
36
43
|
}
|
|
37
44
|
};
|
|
38
45
|
}) });
|
|
@@ -45,7 +52,7 @@ const useReferenceSearch = (targets, views) => {
|
|
|
45
52
|
if ((texts[id] ?? "").trim() === "") return { onSearch };
|
|
46
53
|
return {
|
|
47
54
|
onSearch,
|
|
48
|
-
matches: results[index]?.data ?? []
|
|
55
|
+
matches: (results[index]?.data ?? []).map((item) => named(target, item))
|
|
49
56
|
};
|
|
50
57
|
});
|
|
51
58
|
};
|
package/dist/package.js
CHANGED