@protobase/ui 0.21.4 → 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.
- package/dist/app/embedded-list/cut-off-note.js +11 -0
- package/dist/app/embedded-list/embedded-list.js +14 -5
- package/dist/app/embedded-list/use-embedded-rows.js +15 -3
- package/dist/app/related-section.js +7 -8
- package/dist/live/related-column-rows.js +7 -8
- package/dist/live/search-all.js +27 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/{app-CcsrYoRP.js → app-Dfqcc3td.js} +10 -10
- package/dist/shell/assets/{index-DQ-VSGee.js → index-BztYGbYc.js} +1 -1
- package/dist/shell/assets/{maplibre-4azlIk9w.js → maplibre-CVOEAJ62.js} +1 -1
- package/dist/shell/assets/{markdown-content-DnIl00m7.js → markdown-content-Bg8FC_aK.js} +1 -1
- package/dist/shell/assets/{record-map-CUEC6IyM.js → record-map-ChUVZLR6.js} +1 -1
- package/dist/shell/assets/{records-map-ataN9m4I.js → records-map-BAI2D9VS.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-Cibu09RP.js → shared-protobase-ui-CzT3bJ35.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
|
@@ -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
|
|
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.
|
|
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
|
-
}),
|
|
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
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
|
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, "",
|
|
37
|
+
queryKey: keys.list(via?.name ?? "", viaFilter, "", 0, `related:${key}`),
|
|
38
38
|
enabled: Boolean(via && key),
|
|
39
39
|
queryFn: async () => {
|
|
40
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
43
|
-
*
|
|
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
|
|
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,
|
|
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