@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.
Files changed (42) hide show
  1. package/dist/app/app.js +1 -1
  2. package/dist/app/displays/record-card.js +8 -0
  3. package/dist/app/embedded-list/embedded-columns.js +2 -1
  4. package/dist/app/embedded-list/use-embedded-rows.js +6 -2
  5. package/dist/app/list-page.js +6 -2
  6. package/dist/app/nav-from-meta.js +19 -3
  7. package/dist/app/pages/field-value.js +3 -14
  8. package/dist/app/record-page.js +1 -1
  9. package/dist/app/related-cards/related-card.js +1 -1
  10. package/dist/app/relation-picker.js +12 -4
  11. package/dist/app/titles/record-titles.js +1 -1
  12. package/dist/app/titles/relation-title-cells.js +28 -0
  13. package/dist/app/titles/relation-title-text.js +7 -0
  14. package/dist/app/titles/relation-title.js +3 -11
  15. package/dist/app/titles/use-relation-title.js +25 -0
  16. package/dist/app/use-related-records.js +1 -1
  17. package/dist/app-shell/sidebar-create-link.js +26 -0
  18. package/dist/app-shell/sidebar-group.js +6 -14
  19. package/dist/app-shell/sidebar-rows.js +13 -0
  20. package/dist/app-shell/sidebar.js +11 -5
  21. package/dist/index.js +2 -2
  22. package/dist/live/grid-loader.js +5 -3
  23. package/dist/live/relation-labels.js +5 -2
  24. package/dist/live/title-rows.js +47 -0
  25. package/dist/live/use-filter-config.js +27 -6
  26. package/dist/live/use-reference-search.js +14 -7
  27. package/dist/package.js +1 -1
  28. package/dist/shell/assets/app-CAj-S5YI.js +9 -0
  29. package/dist/shell/assets/{index-BX1N2n2W.css → index-BRYcNA4H.css} +1 -1
  30. package/dist/shell/assets/{index-DprdVbQM.js → index-CEhwvXIf.js} +2 -2
  31. package/dist/shell/assets/{records-map-DhDIjikh.js → records-map-Cp4sUeC-.js} +2 -2
  32. package/dist/shell/assets/{shared-protobase-ui-uXIvgyTZ.js → shared-protobase-ui-Dd4pdw8C.js} +2 -2
  33. package/dist/shell/index.html +4 -4
  34. package/dist/shell/protobase.shell.json +1 -1
  35. package/dist/shell/protobase.sw.js +2 -2
  36. package/dist/shell-source-maps/assets/app-CAj-S5YI.js.map +1 -0
  37. package/dist/shell-source-maps/assets/{index-DprdVbQM.js.map → index-CEhwvXIf.js.map} +1 -1
  38. package/dist/shell-source-maps/assets/{records-map-DhDIjikh.js.map → records-map-Cp4sUeC-.js.map} +1 -1
  39. package/dist/shell-source-maps/assets/{shared-protobase-ui-uXIvgyTZ.js.map → shared-protobase-ui-Dd4pdw8C.js.map} +1 -1
  40. package/package.json +4 -4
  41. package/dist/shell/assets/app-DxuzNqzR.js +0 -9
  42. 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((counts(group.id, group.id) ?? []).flatMap((facet) => facet.value === null ? [] : [String(facet.value)]), state.facets[group.id] ?? []);
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([...named, ...labelQueries[index]?.data ?? []]);
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: [keyField, labelField],
40
+ fields: nameFields(target),
34
41
  pageSize: 20
35
- })).items.map((item) => [String(item[keyField]), String(item[labelField])]);
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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.55.1";
2
+ var version = "0.55.3";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };