@protobase/ui 0.21.5 → 0.21.7

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 (70) hide show
  1. package/dist/app/app.d.ts +3 -3
  2. package/dist/app/app.js +32 -13
  3. package/dist/app/create/create-page.js +4 -4
  4. package/dist/app/custom-section.js +1 -1
  5. package/dist/app/displays/board-columns.js +1 -1
  6. package/dist/app/displays/board-display.js +1 -1
  7. package/dist/app/displays/list-displays.js +1 -1
  8. package/dist/app/displays/map-display.js +1 -1
  9. package/dist/app/displays/record-card.js +2 -2
  10. package/dist/app/displays/use-board-column.js +1 -1
  11. package/dist/app/displays/use-board-move.js +1 -1
  12. package/dist/app/displays/use-map-records.js +1 -1
  13. package/dist/app/embedded-list/cut-off-note.js +11 -0
  14. package/dist/app/embedded-list/embedded-list.js +17 -8
  15. package/dist/app/embedded-list/use-embedded-rows.js +16 -4
  16. package/dist/app/gallery/gallery-section.js +1 -1
  17. package/dist/app/gallery/lightbox.js +1 -1
  18. package/dist/app/hand-order/hand-sorted-list.js +3 -3
  19. package/dist/app/hand-order/use-hand-order.js +2 -2
  20. package/dist/app/import/import-dialog.js +1 -1
  21. package/dist/app/list-edit/use-list-edits.js +1 -1
  22. package/dist/app/list-page.js +4 -4
  23. package/dist/app/map/map-section.js +1 -1
  24. package/dist/app/pages/action-inputs/action-inputs-dialog.js +3 -3
  25. package/dist/app/pages/blocks/card-row.js +1 -1
  26. package/dist/app/pages/blocks/modal-form.js +3 -3
  27. package/dist/app/pages/blocks/record-card.js +1 -1
  28. package/dist/app/pages/field-value.js +1 -1
  29. package/dist/app/pages/use-run-action.js +1 -1
  30. package/dist/app/record-lines.js +1 -1
  31. package/dist/app/record-page.js +5 -5
  32. package/dist/app/record-row/record-row-body.js +1 -1
  33. package/dist/app/record-row/saved-record-row.js +1 -1
  34. package/dist/app/related-dots/related-dots.js +1 -1
  35. package/dist/app/related-section.js +8 -9
  36. package/dist/app/relation-picker.js +1 -1
  37. package/dist/app/search/search-model.js +1 -1
  38. package/dist/app/searches/saved-search-rows.js +2 -2
  39. package/dist/app/searches/saved-searches.js +2 -2
  40. package/dist/app/searches/update-search-dialog.js +1 -1
  41. package/dist/app/searches/use-save-search.js +1 -1
  42. package/dist/app/searches/use-saved-searches.js +1 -1
  43. package/dist/app/use-app-address.js +1 -1
  44. package/dist/assistant-dock/widget-view.js +1 -1
  45. package/dist/auth/auth-layout.d.ts +4 -2
  46. package/dist/auth/auth-layout.js +4 -3
  47. package/dist/data/api-provider.d.ts +6 -0
  48. package/dist/data/query-keys.js +3 -1
  49. package/dist/forms/prove-human.js +24 -0
  50. package/dist/forms/public-form-page.js +142 -0
  51. package/dist/forms/use-form-address.js +11 -0
  52. package/dist/index.js +7 -7
  53. package/dist/live/columns-from-model.js +1 -1
  54. package/dist/live/related-column-rows.js +7 -8
  55. package/dist/live/search-all.js +27 -0
  56. package/dist/live/use-filter-config.js +1 -1
  57. package/dist/package.js +1 -1
  58. package/dist/plugins/plugin-gate.js +1 -1
  59. package/dist/shell/assets/app-BbVdHzEY.js +69 -0
  60. package/dist/shell/assets/{index-BMiDZRGI.js → index-CQc-60lQ.js} +1 -1
  61. package/dist/shell/assets/{maplibre-BFMEpd4m.js → maplibre-BvtgTvfx.js} +1 -1
  62. package/dist/shell/assets/{markdown-content-yqq0wmAS.js → markdown-content-CJ8g5iKO.js} +1 -1
  63. package/dist/shell/assets/{record-map-DhpikXZs.js → record-map-DSXYVoqB.js} +1 -1
  64. package/dist/shell/assets/{records-map-LDeVJQab.js → records-map-cU_FAK7w.js} +1 -1
  65. package/dist/shell/assets/shared-protobase-ui-Bc0CoTXD.js +1 -0
  66. package/dist/shell/index.html +3 -3
  67. package/dist/shell/protobase.shell.json +1 -1
  68. package/package.json +5 -4
  69. package/dist/shell/assets/app-BmBlR0fJ.js +0 -69
  70. package/dist/shell/assets/shared-protobase-ui-DfPkkHCn.js +0 -1
@@ -1,10 +1,11 @@
1
1
  import { Skeleton } from "../primitives/skeleton.js";
2
2
  import { keys } from "../data/query-keys.js";
3
3
  import { useClient } from "../data/api-provider.js";
4
- import { useAdminMeta, usePermissions } from "./meta-gate.js";
5
4
  import { Section } from "../record-view/section.js";
6
5
  import { ErrorBanner } from "./error-banner.js";
7
6
  import { listColumnNames } from "../live/columns-from-model.js";
7
+ import { useAdminMeta, usePermissions } from "./meta-gate.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,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Input } from "../primitives/input.js";
3
- import { labelFieldOf } from "../live/model-helpers.js";
4
3
  import { humanize } from "../live/naming.js";
4
+ import { labelFieldOf } from "../live/model-helpers.js";
5
5
  import { useList } from "../data/use-list.js";
6
6
  import { relationOptionsQuery, titleMatches } from "./relation-options.js";
7
7
  import { useCallback, useEffect, useId, useRef, useState } from "react";
@@ -1,5 +1,5 @@
1
- import { recordKey, recordTitleField } from "../../live/model-helpers.js";
2
1
  import { humanize } from "../../live/naming.js";
2
+ import { recordKey, recordTitleField } from "../../live/model-helpers.js";
3
3
  import { appPath, createWhere, enumLabel, printFilter } from "@protobase/schema";
4
4
  import { encodeKey, matchedSearchField } from "@protobase/schema/internal";
5
5
  //#region src/app/search/search-model.ts
@@ -1,8 +1,8 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
- import { recordKey } from "../../live/model-helpers.js";
3
2
  import { humanize } from "../../live/naming.js";
4
- import { useAdminMeta } from "../meta-gate.js";
5
3
  import { ErrorBanner } from "../error-banner.js";
4
+ import { recordKey } from "../../live/model-helpers.js";
5
+ import { useAdminMeta } from "../meta-gate.js";
6
6
  import { AppLink } from "../use-app-address.js";
7
7
  import { InlineFilterChips } from "./filter-chip-list.js";
8
8
  import { RecordRowBody } from "../record-row/record-row-body.js";
@@ -1,9 +1,9 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { PageHeader } from "../../app-shell/page-header.js";
3
- import { recordKey } from "../../live/model-helpers.js";
4
3
  import { humanize } from "../../live/naming.js";
5
- import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
6
4
  import { ErrorBanner } from "../error-banner.js";
5
+ import { recordKey } from "../../live/model-helpers.js";
6
+ import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
7
7
  import { AppLink } from "../use-app-address.js";
8
8
  import { can } from "../permissions.js";
9
9
  import { useSavedSearches } from "./use-saved-searches.js";
@@ -2,8 +2,8 @@ import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { Button } from "../../primitives/button.js";
3
3
  import { useClient } from "../../data/api-provider.js";
4
4
  import { humanize } from "../../live/naming.js";
5
- import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
6
5
  import { defaultOrderBy } from "../list-order.js";
6
+ import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
7
7
  import { countQuery } from "../pages/use-layout-data.js";
8
8
  import { useFilterChips } from "./use-filter-chips.js";
9
9
  import { EmbeddedList } from "../embedded-list/embedded-list.js";
@@ -1,7 +1,7 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
- import { recordKey } from "../../live/model-helpers.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { useToast } from "../../toasts/toast-provider.js";
4
+ import { recordKey } from "../../live/model-helpers.js";
5
5
  import { useAdminMeta } from "../meta-gate.js";
6
6
  import { useAppAddress } from "../use-app-address.js";
7
7
  import { can } from "../permissions.js";
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
- import { useAdminMeta } from "../meta-gate.js";
4
3
  import { defaultOrderBy } from "../list-order.js";
4
+ import { useAdminMeta } from "../meta-gate.js";
5
5
  import { useQuery } from "@tanstack/react-query";
6
6
  //#region src/app/searches/use-saved-searches.ts
7
7
  /** Where the user's saved searches of `resource` are cached; a list key, so a write to the resource refreshes it. */
@@ -1,7 +1,7 @@
1
1
  import { useIsXs } from "../lib/use-media-query.js";
2
2
  import { Link, useRouter } from "./router.js";
3
- import { useAdminMeta } from "./meta-gate.js";
4
3
  import { changedList, currentAddress, listDefaults, listedResource } from "./current-address.js";
4
+ import { useAdminMeta } from "./meta-gate.js";
5
5
  import { useCallback, useMemo } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import { appPath, readAppPath } from "@protobase/schema";
@@ -1,6 +1,6 @@
1
1
  import { reportRenderError } from "../app/error-reporting.js";
2
- import { ActionCard } from "../action-card/action-card.js";
3
2
  import { useProjectUi } from "../app/pages/project-ui.js";
3
+ import { ActionCard } from "../action-card/action-card.js";
4
4
  import { Component } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  //#region src/assistant-dock/widget-view.tsx
@@ -3,8 +3,10 @@ import { ReactNode } from "react";
3
3
  export type AuthLayoutProps = {
4
4
  title: string;
5
5
  description?: string;
6
+ /** How wide the card is: `sm`, the default, for a few short inputs; `md` for a form with longer ones. */
7
+ size?: 'sm' | 'md';
6
8
  children: ReactNode;
7
9
  };
8
- /** The frame of the sign-in and first-run pages: logo, title and a card. */
9
- export declare const AuthLayout: ({ title, description, children }: AuthLayoutProps) => import("react").JSX.Element;
10
+ /** The frame of the sign-in and first-run pages and of public forms: logo, title and a card. */
11
+ export declare const AuthLayout: ({ title, description, size, children }: AuthLayoutProps) => import("react").JSX.Element;
10
12
  //#endregion
@@ -1,11 +1,12 @@
1
+ import { cn } from "../lib/cn.js";
1
2
  import { LogoMark } from "../app-shell/logo.js";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
4
  //#region src/auth/auth-layout.tsx
4
- /** The frame of the sign-in and first-run pages: logo, title and a card. */
5
- const AuthLayout = ({ title, description, children }) => /* @__PURE__ */ jsx("div", {
5
+ /** The frame of the sign-in and first-run pages and of public forms: logo, title and a card. */
6
+ const AuthLayout = ({ title, description, size = "sm", children }) => /* @__PURE__ */ jsx("div", {
6
7
  className: "flex min-h-full w-full items-center justify-center bg-surface px-4 py-10",
7
8
  children: /* @__PURE__ */ jsxs("div", {
8
- className: "w-full max-w-sm",
9
+ className: cn("w-full", size === "sm" ? "max-w-sm" : "max-w-lg"),
9
10
  children: [/* @__PURE__ */ jsxs("div", {
10
11
  className: "mb-6 flex flex-col items-center text-center",
11
12
  children: [
@@ -43,6 +43,9 @@ export declare const useClient: () => {
43
43
  }) => Promise<import("@protobase/client").MetaResult>;
44
44
  invoke: <T = unknown>(name: string, { method, body, path, query, headers }?: import("@protobase/client").InvokeOptions) => Promise<T>;
45
45
  upload: (resource: import("@protobase/client").ResourceRef, field: string, file: Blob, { onProgress, signal }?: import("@protobase/client").UploadOptions) => Promise<import("@protobase/client").UploadResult>;
46
+ form: (name: string) => Promise<import("@protobase/client").PublicForm>;
47
+ formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
48
+ submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
46
49
  };
47
50
  /**
48
51
  * The client of the API that serves `resource`: a source's for a resource the app draws from one, the app's own
@@ -88,6 +91,9 @@ export declare const useResourceClient: (resource: string) => {
88
91
  }) => Promise<import("@protobase/client").MetaResult>;
89
92
  invoke: <T = unknown>(name: string, { method, body, path, query, headers }?: import("@protobase/client").InvokeOptions) => Promise<T>;
90
93
  upload: (resource: import("@protobase/client").ResourceRef, field: string, file: Blob, { onProgress, signal }?: import("@protobase/client").UploadOptions) => Promise<import("@protobase/client").UploadResult>;
94
+ form: (name: string) => Promise<import("@protobase/client").PublicForm>;
95
+ formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
96
+ submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
91
97
  };
92
98
  export type ApiProviderProps = {
93
99
  /** Defaults to `/api/v1` on the current origin. */
@@ -134,7 +134,9 @@ const keys = {
134
134
  resource,
135
135
  key,
136
136
  path
137
- ]
137
+ ],
138
+ /** A public form, as `/-/forms/<name>` draws it. */
139
+ publicForm: (name) => ["public-form", name]
138
140
  };
139
141
  //#endregion
140
142
  export { keys };
@@ -0,0 +1,24 @@
1
+ import { solveChallenge } from "altcha-lib";
2
+ import { deriveKey } from "altcha-lib/algorithms/web/sha";
3
+ //#region src/forms/prove-human.ts
4
+ /**
5
+ * The bot check of a public form, done: fetches a fresh challenge and solves it in this browser, about a second of its
6
+ * work, so a check never expires while someone types.
7
+ */
8
+ const proveHuman = async (client, name) => {
9
+ const challenge = await client.formChallenge(name);
10
+ const solution = await solveChallenge({
11
+ challenge,
12
+ deriveKey
13
+ });
14
+ if (!solution) throw new Error("The check that you are not a bot took too long; send the form again");
15
+ return {
16
+ challenge,
17
+ solution: {
18
+ counter: solution.counter,
19
+ derivedKey: solution.derivedKey
20
+ }
21
+ };
22
+ };
23
+ //#endregion
24
+ export { proveHuman };
@@ -0,0 +1,142 @@
1
+ import { Spinner } from "../primitives/spinner.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { keys } from "../data/query-keys.js";
4
+ import { useClient } from "../data/api-provider.js";
5
+ import { AuthLayout } from "../auth/auth-layout.js";
6
+ import { humanize } from "../live/naming.js";
7
+ import { Field } from "../record-view/field.js";
8
+ import { createBody, hasDatabaseDefault, initialDraft, isRequired } from "../app/create/create-values.js";
9
+ import { useDocumentTitle } from "../app/document-title.js";
10
+ import { ErrorBanner } from "../app/error-banner.js";
11
+ import { Notice } from "../app/meta-gate.js";
12
+ import { FieldEditor } from "../app/field-editor.js";
13
+ import { fieldErrors, fieldMessage } from "../app/field-errors.js";
14
+ import { useFocusFirstInvalid } from "../app/use-focus-first-invalid.js";
15
+ import { proveHuman } from "./prove-human.js";
16
+ import { useState } from "react";
17
+ import { isApiError } from "@protobase/client";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ import { useQuery } from "@tanstack/react-query";
20
+ //#region src/forms/public-form-page.tsx
21
+ /**
22
+ * `/-/forms/<name>`: a public form, for anyone, signed in or not. It sends with a bot check its browser solves, and
23
+ * then shows the form's thanks in its place.
24
+ */
25
+ const PublicFormPage = ({ name }) => {
26
+ const client = useClient();
27
+ const form = useQuery({
28
+ queryKey: keys.publicForm(name),
29
+ queryFn: () => client.form(name)
30
+ });
31
+ if (form.isPending) return /* @__PURE__ */ jsxs("div", {
32
+ className: "flex h-full min-h-48 items-center justify-center gap-2 text-muted-foreground",
33
+ children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
34
+ });
35
+ if (form.error) {
36
+ if (isApiError(form.error) && form.error.status === 404) return /* @__PURE__ */ jsx(Notice, {
37
+ title: "Not found",
38
+ children: "There is no form at this address."
39
+ });
40
+ return /* @__PURE__ */ jsx(Notice, {
41
+ title: "Cannot reach the API",
42
+ children: isApiError(form.error) ? form.error.message : "The server did not answer. Is it running?"
43
+ });
44
+ }
45
+ return /* @__PURE__ */ jsx(LoadedForm, { form: form.data });
46
+ };
47
+ const LoadedForm = ({ form }) => {
48
+ const client = useClient();
49
+ const { resource: model, view } = form;
50
+ const title = form.title ?? view.names?.singular ?? humanize(view.resource);
51
+ useDocumentTitle(title);
52
+ const [draft, setDraft] = useState(() => initialDraft(model, [{
53
+ title,
54
+ fields: form.fields
55
+ }]));
56
+ const [errors, setErrors] = useState({});
57
+ const [failure, setFailure] = useState();
58
+ const [sending, setSending] = useState(false);
59
+ const [sent, setSent] = useState(false);
60
+ const [rejections, setRejections] = useState(0);
61
+ const formRef = useFocusFirstInvalid(rejections);
62
+ if (sent) return /* @__PURE__ */ jsx(AuthLayout, {
63
+ title,
64
+ size: "md",
65
+ children: /* @__PURE__ */ jsx("p", {
66
+ role: "status",
67
+ className: "rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
68
+ children: form.thanks
69
+ })
70
+ });
71
+ const change = (name, value) => {
72
+ setErrors((current) => Object.fromEntries(Object.entries(current).filter(([field]) => field !== name)));
73
+ setDraft((current) => ({
74
+ ...current,
75
+ [name]: value
76
+ }));
77
+ };
78
+ const send = async () => {
79
+ setSending(true);
80
+ setFailure(void 0);
81
+ try {
82
+ await client.submitForm(form.name, createBody(model, {}, draft), await proveHuman(client, form.name));
83
+ setSent(true);
84
+ } catch (error) {
85
+ const found = fieldErrors(error);
86
+ setErrors(found);
87
+ if (Object.keys(found).length > 0) setRejections((count) => count + 1);
88
+ else setFailure(error);
89
+ } finally {
90
+ setSending(false);
91
+ }
92
+ };
93
+ return /* @__PURE__ */ jsx(AuthLayout, {
94
+ title,
95
+ size: "md",
96
+ children: /* @__PURE__ */ jsxs("form", {
97
+ ref: formRef,
98
+ className: "flex flex-col gap-4",
99
+ onSubmit: (event) => {
100
+ event.preventDefault();
101
+ send();
102
+ },
103
+ children: [
104
+ form.fields.map((name) => {
105
+ const field = model.fields[name];
106
+ const hints = view.fields[name];
107
+ const label = hints?.label ?? humanize(name);
108
+ const problem = errors[name];
109
+ return /* @__PURE__ */ jsx(Field, {
110
+ name,
111
+ label,
112
+ help: hints?.help,
113
+ required: isRequired(field),
114
+ error: problem && fieldMessage(field, label, problem),
115
+ children: /* @__PURE__ */ jsx(FieldEditor, {
116
+ field,
117
+ hints,
118
+ value: draft[name] ?? (field.type === "boolean" ? false : ""),
119
+ stored: void 0,
120
+ placeholder: hasDatabaseDefault(field) ? "Set by the database" : void 0,
121
+ invalid: Boolean(problem),
122
+ onChange: (next) => change(name, next)
123
+ })
124
+ }, name);
125
+ }),
126
+ failure !== void 0 && /* @__PURE__ */ jsx(ErrorBanner, {
127
+ title: "Not sent",
128
+ error: failure
129
+ }),
130
+ /* @__PURE__ */ jsx(Button, {
131
+ variant: "primary",
132
+ type: "submit",
133
+ loading: sending,
134
+ className: "min-h-10",
135
+ children: "Send"
136
+ })
137
+ ]
138
+ })
139
+ });
140
+ };
141
+ //#endregion
142
+ export { PublicFormPage };
@@ -0,0 +1,11 @@
1
+ import { useRouter } from "../app/router.js";
2
+ import { useMemo } from "react";
3
+ import { readFormAddress } from "@protobase/schema";
4
+ //#region src/forms/use-form-address.ts
5
+ /** The public form the address shows, `/-/forms/<name>`, as logical values; `undefined` on any other page. */
6
+ const useFormAddress = () => {
7
+ const { path } = useRouter();
8
+ return useMemo(() => readFormAddress(path), [path]);
9
+ };
10
+ //#endregion
11
+ export { useFormAddress };
package/dist/index.js CHANGED
@@ -16,6 +16,13 @@ import { usePlugin } from "./plugins/plugin-context.js";
16
16
  import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
17
17
  import { useApiToken } from "./auth/use-api-token.js";
18
18
  import { useRecord } from "./data/use-record.js";
19
+ import { Field } from "./record-view/field.js";
20
+ import { Section } from "./record-view/section.js";
21
+ import { Textarea } from "./primitives/textarea.js";
22
+ import { useMeta } from "./data/use-meta.js";
23
+ import { AppLink, useAppAddress } from "./app/use-app-address.js";
24
+ import { useList } from "./data/use-list.js";
25
+ import { definePlugin, defineUi } from "./app/pages/project-ui.js";
19
26
  import { ChatMessage } from "./chat/chat-message.js";
20
27
  import { ChatThread } from "./chat/chat-thread.js";
21
28
  import { Composer } from "./chat/composer.js";
@@ -26,19 +33,12 @@ import { CompactTable } from "./compact-table/compact-table.js";
26
33
  import { diffStats, parseDiff } from "./diff-view/diff.js";
27
34
  import { DiffView } from "./diff-view/diff-view.js";
28
35
  import { StepList } from "./step-list/step-list.js";
29
- import { definePlugin, defineUi } from "./app/pages/project-ui.js";
30
36
  import { PartView } from "./assistant-dock/part-view.js";
31
37
  import { useBackendData } from "./assistant-dock/use-backend-data.js";
32
38
  import { AssistantDock } from "./assistant-dock/assistant-dock.js";
33
39
  import { useAssistant } from "./assistant-dock/use-assistant.js";
34
40
  import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
35
- import { useMeta } from "./data/use-meta.js";
36
41
  import { ResponsiveDialog } from "./primitives/responsive-dialog.js";
37
- import { Field } from "./record-view/field.js";
38
- import { Section } from "./record-view/section.js";
39
- import { Textarea } from "./primitives/textarea.js";
40
- import { AppLink, useAppAddress } from "./app/use-app-address.js";
41
- import { useList } from "./data/use-list.js";
42
42
  import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
43
43
  import { useLayoutRecord } from "./app/pages/record-scope.js";
44
44
  import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
@@ -1,8 +1,8 @@
1
- import { recordTitleField } from "./model-helpers.js";
2
1
  import { humanize } from "./naming.js";
3
2
  import { badgeWidth, headerWidth } from "../data-grid/content-widths.js";
4
3
  import { markdownExcerpt } from "../markdown/markdown-excerpt.js";
5
4
  import { isMarkdown } from "../markdown/markdown-hint.js";
5
+ import { recordTitleField } from "./model-helpers.js";
6
6
  import { enumLabel } from "@protobase/schema";
7
7
  //#region src/live/columns-from-model.ts
8
8
  const tones = {
@@ -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 };
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../data/query-keys.js";
2
2
  import { useClient } from "../data/api-provider.js";
3
- import { labelFieldOf } from "./model-helpers.js";
4
3
  import { humanize } from "./naming.js";
4
+ import { labelFieldOf } from "./model-helpers.js";
5
5
  import { fetchLabels } from "./relation-labels.js";
6
6
  import { setRange } from "../filter-panel/filter-state.js";
7
7
  import { stateToText } from "./filter-string.js";
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.7";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -2,8 +2,8 @@ import { Spinner } from "../primitives/spinner.js";
2
2
  import { mergePlugins } from "./merge-plugins.js";
3
3
  import { usePluginSource } from "./plugin-source.js";
4
4
  import { usePluginLoads } from "./use-plugin-loads.js";
5
- import { ProjectUiContext } from "../app/pages/project-ui.js";
6
5
  import { useAdminMeta } from "../app/meta-gate.js";
6
+ import { ProjectUiContext } from "../app/pages/project-ui.js";
7
7
  import { createContext, useContext, useMemo } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/plugins/plugin-gate.tsx