@protobase/ui 0.21.5 → 0.21.6

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.
@@ -0,0 +1,11 @@
1
+ //#region src/app/embedded-list/cut-off-note.ts
2
+ /**
3
+ * What a list that shows only its first `shown` of `total` records says about the rest, as "The first 10 of 37.";
4
+ * nothing when it shows them all. An `estimated` total is the planner's, so it reads "about".
5
+ */
6
+ const cutOffNote = (shown, total, estimated) => {
7
+ if (shown >= total) return void 0;
8
+ return `The first ${shown} of ${estimated ? "about " : ""}${total.toLocaleString("en-GB")}.`;
9
+ };
10
+ //#endregion
11
+ export { cutOffNote };
@@ -17,6 +17,7 @@ import { CustomDisplay } from "../displays/custom-display.js";
17
17
  import { MapDisplay } from "../displays/map-display.js";
18
18
  import { DisplaySwitch } from "../displays/display-controls.js";
19
19
  import { embeddedColumns } from "./embedded-columns.js";
20
+ import { cutOffNote } from "./cut-off-note.js";
20
21
  import { useEmbeddedRows } from "./use-embedded-rows.js";
21
22
  import { useMemo, useState } from "react";
22
23
  import { ArrowRight } from "lucide-react";
@@ -35,7 +36,8 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
35
36
  });
36
37
  /**
37
38
  * The first `limit` records of a list, drawn by the list page's own table (sized to its rows) or display, and a link
38
- * to the whole list with the same filter, order and display. On a phone a cards display is one row that scrolls sideways.
39
+ * to the whole list with the same filter, order and display, beside a note such as "The first 10 of 37." when there are
40
+ * more. On a phone a cards display is one row that scrolls sideways.
39
41
  */
40
42
  const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll, link = true }) => {
41
43
  const { resources, views } = useAdminMeta();
@@ -68,7 +70,7 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
68
70
  limit,
69
71
  fields
70
72
  });
71
- const loaded = rows.data;
73
+ const loaded = rows.data?.rows;
72
74
  const source = useMemo(() => loaded && {
73
75
  total: loaded.length,
74
76
  rowAt: (index) => loaded[index]
@@ -113,8 +115,8 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
113
115
  }
114
116
  const list = {
115
117
  resource: model.name,
116
- rows: rows.data.map((row) => displayRow(model, row)),
117
- total: rows.data.length,
118
+ rows: rows.data.rows.map((row) => displayRow(model, row)),
119
+ total: rows.data.total,
118
120
  loading: false,
119
121
  error: null,
120
122
  more: false,
@@ -134,6 +136,7 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
134
136
  onFollow: () => openRecord(row)
135
137
  });
136
138
  const singular = view?.names?.singular ?? humanize(model.name);
139
+ const note = cutOffNote(source.total, rows.data.total, rows.data.estimated);
137
140
  return /* @__PURE__ */ jsxs("div", {
138
141
  className: "flex flex-col gap-3",
139
142
  children: [!shown ? /* @__PURE__ */ jsx(DataGrid, {
@@ -177,7 +180,13 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
177
180
  }) : /* @__PURE__ */ jsx(CustomDisplay, {
178
181
  display: shown,
179
182
  list
180
- }), allLink]
183
+ }), note && link ? /* @__PURE__ */ jsxs("div", {
184
+ className: "flex flex-wrap items-center justify-between gap-2",
185
+ children: [/* @__PURE__ */ jsx("p", {
186
+ className: "text-xs text-muted-foreground",
187
+ children: note
188
+ }), allLink]
189
+ }) : allLink]
181
190
  });
182
191
  };
183
192
  //#endregion
@@ -8,8 +8,9 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
8
8
  //#region src/app/embedded-list/use-embedded-rows.ts
9
9
  /**
10
10
  * The first `limit` records in `orderBy` order, as the list page's table and displays get them: relations among
11
- * `fields` as `{ id, name }`, named in the same call, with the photos of its cards. A filter through a join resource may hold hundreds of keys, so it
12
- * goes in a JSON body.
11
+ * `fields` as `{ id, name }`, named in the same call, with the photos of its cards, and how many records match in all,
12
+ * counted in the same call: `estimated` when more than the server counts exactly match. A filter through a join
13
+ * resource may hold hundreds of keys, so it goes in a JSON body.
13
14
  */
14
15
  const useEmbeddedRows = ({ model, filter, orderBy, limit, fields }) => {
15
16
  const client = useClient();
@@ -24,13 +25,24 @@ const useEmbeddedRows = ({ model, filter, orderBy, limit, fields }) => {
24
25
  filter: filter || void 0,
25
26
  orderBy: orderBy || void 0,
26
27
  pageSize: limit,
28
+ count: "exact",
27
29
  labels,
28
30
  photos
29
31
  });
30
- return withLabels(resources, model, fields, withPhotos(model, page.items, page.photos), page.labels).map((row, index) => ({
32
+ const rows = withLabels(resources, model, fields, withPhotos(model, page.items, page.photos), page.labels).map((row, index) => ({
31
33
  ...row,
32
34
  id: recordId(model, page.items[index])
33
35
  }));
36
+ if (page.nextPageToken === "") return {
37
+ rows,
38
+ total: rows.length,
39
+ estimated: false
40
+ };
41
+ return {
42
+ rows,
43
+ total: page.totalSize ?? page.totalSizeEstimate,
44
+ estimated: page.totalSize === void 0
45
+ };
34
46
  }
35
47
  });
36
48
  };
@@ -5,6 +5,7 @@ import { useAdminMeta, usePermissions } from "./meta-gate.js";
5
5
  import { Section } from "../record-view/section.js";
6
6
  import { ErrorBanner } from "./error-banner.js";
7
7
  import { listColumnNames } from "../live/columns-from-model.js";
8
+ import { searchAll } from "../live/search-all.js";
8
9
  import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
9
10
  import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
10
11
  import { relatedHandOrder } from "./hand-order/hand-order.js";
@@ -16,15 +17,14 @@ import { useQuery } from "@tanstack/react-query";
16
17
  import { createWhere, printFilter } from "@protobase/schema";
17
18
  //#region src/app/related-section.tsx
18
19
  const where$1 = createWhere();
19
- /** The most join records a section reads to find its records; the list API's largest page. */
20
- const maxThrough = 500;
21
20
  const and = (...parts) => {
22
21
  const given = parts.filter((part) => Boolean(part));
23
22
  return given.length === 1 ? given[0] : given.map((part) => `(${part})`).join(" AND ");
24
23
  };
25
24
  /**
26
25
  * The filter on the section's resource: its `field` is this record's key, or, through a join resource, one of the keys
27
- * the join records that point at this record hold. `null` when no join record does, so nothing can match.
26
+ * all the join records that point at this record hold, read page by page. `null` when no join record does, so nothing
27
+ * can match.
28
28
  */
29
29
  const useMatch = (item, recordKey) => {
30
30
  const client = useClient();
@@ -34,15 +34,14 @@ const useMatch = (item, recordKey) => {
34
34
  const key = through?.key ?? via?.primaryKey[0];
35
35
  const viaFilter = through ? and(printFilter(where$1.eq(through.field, recordKey)), through.filter) : "";
36
36
  const collected = useQuery({
37
- queryKey: keys.list(via?.name ?? "", viaFilter, "", maxThrough, `related:${key}`),
37
+ queryKey: keys.list(via?.name ?? "", viaFilter, "", 0, `related:${key}`),
38
38
  enabled: Boolean(via && key),
39
39
  queryFn: async () => {
40
- const page = await client.search(via.name, {
40
+ const joined = await searchAll(client, via.name, {
41
41
  filter: viaFilter,
42
- fields: [key],
43
- pageSize: maxThrough
42
+ fields: [key]
44
43
  });
45
- return [...new Set(page.items.flatMap((row) => row[key] === null || row[key] === void 0 ? [] : [String(row[key])]))];
44
+ return [...new Set(joined.items.flatMap((row) => row[key] === null || row[key] === void 0 ? [] : [String(row[key])]))];
46
45
  }
47
46
  });
48
47
  if (!through) return {
@@ -1,4 +1,5 @@
1
1
  import { listLabels, withLabels } from "./relation-labels.js";
2
+ import { searchAll } from "./search-all.js";
2
3
  import { createWhere, printFilter } from "@protobase/schema";
3
4
  //#region src/live/related-column-rows.ts
4
5
  const where$1 = createWhere();
@@ -28,10 +29,9 @@ const linksOf = async (client, resources, column, owners) => {
28
29
  const via = resources[through.resource];
29
30
  const key = through.key ?? via?.primaryKey[0];
30
31
  if (!via || key === void 0) throw new Error(`The related column ${column.name} names a join resource the app does not have: ${through.resource}`);
31
- return (await client.search(via.name, {
32
+ return (await searchAll(client, via.name, {
32
33
  filter: and(printFilter(where$1.in(through.field, owners)), through.filter),
33
- fields: [through.field, key],
34
- pageSize: 500
34
+ fields: [through.field, key]
35
35
  })).items.flatMap((row) => {
36
36
  const owner = text(row[through.field]);
37
37
  const value = text(row[key]);
@@ -39,8 +39,8 @@ const linksOf = async (client, resources, column, owners) => {
39
39
  });
40
40
  };
41
41
  /**
42
- * The records a related column shows for each of `owners`, the keys of one page of the list's rows: its join records in
43
- * one request, then the records they pick in another, in the column's order, their relations labelled.
42
+ * The records a related column shows for each of `owners`, the keys of one page of the list's rows: all its join records,
43
+ * then all the records they pick, each read page by page so no dot is left out, in the column's order, their relations labelled.
44
44
  */
45
45
  const loadRelatedColumn = async ({ client, resources, views, load, owners }) => {
46
46
  const { column, fields } = load;
@@ -57,14 +57,13 @@ const loadRelatedColumn = async ({ client, resources, views, load, owners }) =>
57
57
  ...fields
58
58
  ])];
59
59
  const labels = listLabels(resources, views, target, loaded);
60
- const page = await client.search(target.name, {
60
+ const records = await searchAll(client, target.name, {
61
61
  filter: and(printFilter(where$1.in(column.field, values)), column.filter),
62
62
  ...column.sort && { orderBy: column.sort },
63
63
  fields: loaded,
64
- pageSize: 500,
65
64
  labels
66
65
  });
67
- return groupByOwner(links, withLabels(resources, target, loaded, page.items, page.labels), column.field);
66
+ return groupByOwner(links, withLabels(resources, target, loaded, records.items, records.labels), column.field);
68
67
  };
69
68
  //#endregion
70
69
  export { groupByOwner, loadRelatedColumn };
@@ -0,0 +1,27 @@
1
+ //#region src/live/search-all.ts
2
+ const pageSize = 500;
3
+ /** Every record a search matches, page by page, so none is left out; with `labels`, the names of every page merged. */
4
+ const searchAll = async (client, resource, params) => {
5
+ const items = [];
6
+ const labels = {};
7
+ let pageToken;
8
+ do {
9
+ const page = await client.search(resource, {
10
+ ...params,
11
+ pageSize,
12
+ ...pageToken && { pageToken }
13
+ });
14
+ items.push(...page.items);
15
+ for (const [relation, names] of Object.entries(page.labels ?? {})) labels[relation] = {
16
+ ...labels[relation],
17
+ ...names
18
+ };
19
+ pageToken = page.nextPageToken || void 0;
20
+ } while (pageToken);
21
+ return {
22
+ items,
23
+ labels
24
+ };
25
+ };
26
+ //#endregion
27
+ export { searchAll };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.21.5";
2
+ var version = "0.21.6";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };