@protobase/ui 0.21.10 → 0.21.12

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 (55) hide show
  1. package/dist/app/app.js +1 -1
  2. package/dist/app/comments/comment-composer.js +1 -1
  3. package/dist/app/create/create-page.js +1 -1
  4. package/dist/app/create/create-values.js +4 -2
  5. package/dist/app/current-address.d.ts +1 -0
  6. package/dist/app/current-address.js +2 -1
  7. package/dist/app/displays/board-column.js +1 -1
  8. package/dist/app/displays/calendar-display.js +1 -1
  9. package/dist/app/displays/list-cards.js +1 -1
  10. package/dist/app/displays/map-display.js +1 -1
  11. package/dist/app/embedded-list/embedded-list.js +1 -1
  12. package/dist/app/field-editor.js +3 -3
  13. package/dist/app/gallery/detail-fields.js +2 -1
  14. package/dist/app/gallery/gallery-section.js +1 -1
  15. package/dist/app/gallery/photo-details.js +1 -1
  16. package/dist/app/group-open.js +7 -0
  17. package/dist/app/hand-order/hand-sorted-list.js +1 -1
  18. package/dist/app/incoming-relations.js +2 -1
  19. package/dist/app/list-page.js +1 -1
  20. package/dist/app/pages/action-inputs/action-inputs-dialog.js +1 -1
  21. package/dist/app/pages/blocks/card-row.js +1 -1
  22. package/dist/app/pages/blocks/modal-form.js +1 -1
  23. package/dist/app/pages/blocks/record-card.js +1 -1
  24. package/dist/app/pages/blocks/show.js +5 -34
  25. package/dist/app/pages/record-actions.js +1 -1
  26. package/dist/app/pages/use-conditions.js +49 -0
  27. package/dist/app/record-mode/record-mode.js +4 -1
  28. package/dist/app/record-mode/shown-layout.js +25 -0
  29. package/dist/app/record-mode/use-shown-layout.js +14 -0
  30. package/dist/app/record-page.js +90 -72
  31. package/dist/app/record-row/record-row-body.js +1 -1
  32. package/dist/app/record-row/saved-record-row.js +1 -1
  33. package/dist/app/related-section.js +10 -1
  34. package/dist/app/searches/saved-search-rows.js +1 -1
  35. package/dist/app/searches/saved-searches.js +1 -1
  36. package/dist/app/sensitive-field.js +1 -1
  37. package/dist/app/use-app-address.d.ts +1 -0
  38. package/dist/app/use-record-editor.js +1 -1
  39. package/dist/forms/public-form-page.js +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/live/field-values.js +1 -1
  42. package/dist/package.js +1 -1
  43. package/dist/record-view/group-section.js +35 -0
  44. package/dist/shell/assets/app-IuP56Dep.js +69 -0
  45. package/dist/shell/assets/{index-CzqIlZPP.css → index-DhW8t8wq.css} +1 -1
  46. package/dist/shell/assets/{index-C_o0i6I2.js → index-GB5YRjlo.js} +1 -1
  47. package/dist/shell/assets/{maplibre-DLYsHwYU.js → maplibre-BLm41BrA.js} +1 -1
  48. package/dist/shell/assets/{markdown-content-B410ow-t.js → markdown-content-Cia4jbSB.js} +1 -1
  49. package/dist/shell/assets/{record-map-DvgWFyLn.js → record-map-C-Svn7nc.js} +1 -1
  50. package/dist/shell/assets/{records-map-CLj0uHGq.js → records-map-D5LZhC54.js} +1 -1
  51. package/dist/shell/assets/{shared-protobase-ui-Bf1h2ktk.js → shared-protobase-ui-D16YbnPr.js} +1 -1
  52. package/dist/shell/index.html +4 -4
  53. package/dist/shell/protobase.shell.json +1 -1
  54. package/package.json +4 -4
  55. package/dist/shell/assets/app-9imuOKuW.js +0 -69
package/dist/app/app.js CHANGED
@@ -15,11 +15,11 @@ import { isAdmin, userToShell } from "../auth/user-to-shell.js";
15
15
  import { useOpenedRecord } from "../data/use-record.js";
16
16
  import { humanize } from "../live/naming.js";
17
17
  import { ToastProvider } from "../toasts/toast-provider.js";
18
- import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
19
18
  import { recordTitle } from "../live/model-helpers.js";
20
19
  import { sourceStatusOf } from "../data/with-sources.js";
21
20
  import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
22
21
  import { useAppAddress } from "./use-app-address.js";
22
+ import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
23
23
  import { ProjectUiContext, useProjectUi } from "./pages/project-ui.js";
24
24
  import { PublicFormPage } from "../forms/public-form-page.js";
25
25
  import { useFormAddress } from "../forms/use-form-address.js";
@@ -1,8 +1,8 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { Button } from "../../primitives/button.js";
3
3
  import { useToast } from "../../toasts/toast-provider.js";
4
- import { Textarea } from "../../primitives/textarea.js";
5
4
  import { refusalReason } from "../displays/board-columns.js";
5
+ import { Textarea } from "../../primitives/textarea.js";
6
6
  import { menuKeyTarget } from "../../primitives/menu-keys.js";
7
7
  import { activeMention, insertMention, mentionedIds } from "./mention-text.js";
8
8
  import { useMentionable } from "./use-mentionable.js";
@@ -1,9 +1,9 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
4
3
  import { recordKey } from "../../live/model-helpers.js";
5
4
  import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
6
5
  import { useAppAddress } from "../use-app-address.js";
6
+ import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
7
7
  import { FieldEditor } from "../field-editor.js";
8
8
  import { fieldErrors, fieldMessage } from "../field-errors.js";
9
9
  import { useFocusFirstInvalid } from "../use-focus-first-invalid.js";
@@ -1,5 +1,7 @@
1
1
  import { isEditable, toPatchValue } from "../../live/field-values.js";
2
2
  import { whenShown } from "../record-mode/record-mode.js";
3
+ import { isRecordCondition } from "@protobase/schema";
4
+ import { layoutItems } from "@protobase/schema/internal";
3
5
  //#region src/app/create/create-values.ts
4
6
  const isChosen = (value) => typeof value === "object" && value !== null && "key" in value;
5
7
  /** Fields the user can set on a new record: not read-only, not the tenant, and not structured data. */
@@ -12,8 +14,8 @@ const isCreatable = (model, field) => field.name !== model.tenant && !field.read
12
14
  const createSections = (model, view) => {
13
15
  const creatable = Object.values(model.fields).filter((field) => isCreatable(model, field)).map((field) => field.name);
14
16
  const placed = /* @__PURE__ */ new Set();
15
- const sections = (view?.layout ?? []).flatMap((item) => {
16
- if (item.kind !== "section" || !whenShown(item.when, true)) return [];
17
+ const sections = layoutItems(view?.layout ?? []).flatMap((item) => {
18
+ if (item.kind !== "section" || isRecordCondition(item.when) || !whenShown(item.when, true)) return [];
17
19
  const fields = item.fields.filter((name) => creatable.includes(name) && !placed.has(name) && whenShown(view?.fields[name]?.when, true));
18
20
  fields.forEach((name) => placed.add(name));
19
21
  return fields.length > 0 ? [{
@@ -21,6 +21,7 @@ export type CurrentAddress = ListAddress | {
21
21
  resource: string;
22
22
  key: string;
23
23
  edit?: true;
24
+ open?: readonly string[];
24
25
  } | {
25
26
  kind: 'create';
26
27
  resource: string;
@@ -32,7 +32,8 @@ const currentAddress = (read, defaults) => {
32
32
  kind: "record",
33
33
  resource: read.resource,
34
34
  key: encodeKey(read.key),
35
- ...read.edit && { edit: true }
35
+ ...read.edit && { edit: true },
36
+ ...read.open && { open: read.open }
36
37
  };
37
38
  if (read?.kind !== "list") return read;
38
39
  return {
@@ -2,8 +2,8 @@ import { cn } from "../../lib/cn.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { Button } from "../../primitives/button.js";
4
4
  import { humanize } from "../../live/naming.js";
5
- import { ErrorBanner } from "../error-banner.js";
6
5
  import { boardCard } from "./board-columns.js";
6
+ import { ErrorBanner } from "../error-banner.js";
7
7
  import { rowAllows } from "../permissions.js";
8
8
  import { ValueBadge } from "../value-badge.js";
9
9
  import { RecordCard } from "./record-card.js";
@@ -2,9 +2,9 @@ import { cn } from "../../lib/cn.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { dayIn, viewerZone } from "../../format/zone.js";
4
4
  import { formatDate, formatWeekday } from "../../format/date.js";
5
- import { ErrorBanner } from "../error-banner.js";
6
5
  import { usePermissions } from "../meta-gate.js";
7
6
  import { AppLink } from "../use-app-address.js";
7
+ import { ErrorBanner } from "../error-banner.js";
8
8
  import { can, rowAllows } from "../permissions.js";
9
9
  import { collision } from "./board-dnd.js";
10
10
  import { byDay, calendarView, gridDays, rangeOf } from "./calendar-range.js";
@@ -1,7 +1,7 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { ErrorBanner } from "../error-banner.js";
4
3
  import { displayRow } from "./list-displays.js";
4
+ import { ErrorBanner } from "../error-banner.js";
5
5
  import { RecordCard } from "./record-card.js";
6
6
  import { VirtualCards } from "./virtual-cards.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -2,9 +2,9 @@ import { cn } from "../../lib/cn.js";
2
2
  import { Button } from "../../primitives/button.js";
3
3
  import { Problem } from "../pages/problem.js";
4
4
  import { humanize } from "../../live/naming.js";
5
- import { ErrorBanner } from "../error-banner.js";
6
5
  import { mapCards } from "./list-displays.js";
7
6
  import { useAdminMeta } from "../meta-gate.js";
7
+ import { ErrorBanner } from "../error-banner.js";
8
8
  import { useDebounced } from "../../lib/use-debounced.js";
9
9
  import { RecordCard } from "./record-card.js";
10
10
  import { boundsAround, worldArea } from "../map/map-area.js";
@@ -1,12 +1,12 @@
1
1
  import { useIsDesktop, useIsXs } from "../../lib/use-media-query.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { humanize } from "../../live/naming.js";
4
- import { ErrorBanner } from "../error-banner.js";
5
4
  import { listColumnNames } from "../../live/columns-from-model.js";
6
5
  import { displayRow, loadedFields, shownDisplay } from "../displays/list-displays.js";
7
6
  import { defaultOrderBy, parseSort, sortToOrderBy } from "../list-order.js";
8
7
  import { useAdminMeta, usePermissions } from "../meta-gate.js";
9
8
  import { AppLink, useAppAddress } from "../use-app-address.js";
9
+ import { ErrorBanner } from "../error-banner.js";
10
10
  import { useProjectUi } from "../pages/project-ui.js";
11
11
  import { can } from "../permissions.js";
12
12
  import { DataGrid } from "../../data-grid/data-grid.js";
@@ -3,13 +3,13 @@ import { useFieldControl } from "../primitives/field-control.js";
3
3
  import { Input } from "../primitives/input.js";
4
4
  import { buttonClass } from "../primitives/button.js";
5
5
  import { humanize } from "../live/naming.js";
6
- import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
7
- import { Switch } from "../primitives/switch.js";
8
- import { Textarea } from "../primitives/textarea.js";
9
6
  import { isMarkdown } from "../markdown/markdown-hint.js";
10
7
  import { contactFormat, contactInputProps } from "../live/contact-links.js";
11
8
  import { displayValue, valueTitle } from "../live/display-value.js";
12
9
  import { AppLink } from "./use-app-address.js";
10
+ import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
11
+ import { Switch } from "../primitives/switch.js";
12
+ import { Textarea } from "../primitives/textarea.js";
13
13
  import { RelationPicker } from "./relation-picker.js";
14
14
  import { SensitiveField } from "./sensitive-field.js";
15
15
  import { FileField } from "./file-field.js";
@@ -1,4 +1,5 @@
1
1
  import { whenShown } from "../record-mode/record-mode.js";
2
+ import { layoutItems } from "@protobase/schema/internal";
2
3
  //#region src/app/gallery/detail-fields.ts
3
4
  /**
4
5
  * The fields of a photo's details: those its record page shows in its sections, or without a layout every field but its
@@ -6,7 +7,7 @@ import { whenShown } from "../record-mode/record-mode.js";
6
7
  * field pointing at the gallery's record.
7
8
  */
8
9
  const detailFields = (model, view, omit) => {
9
- const sections = (view?.layout ?? []).flatMap((item) => item.kind === "section" && whenShown(item.when, false) ? item.fields : []);
10
+ const sections = layoutItems(view?.layout ?? []).flatMap((item) => item.kind === "section" && whenShown(item.when, false) ? item.fields : []);
10
11
  return (sections.length > 0 ? [...new Set(sections)] : Object.keys(model.fields).filter((name) => name !== model.tenant && !model.primaryKey.includes(name))).filter((name) => model.fields[name] && !model.fields[name].sensitive && !omit.includes(name) && whenShown(view?.fields[name]?.when, false));
11
12
  };
12
13
  //#endregion
@@ -3,8 +3,8 @@ import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { humanize } from "../../live/naming.js";
4
4
  import { Section } from "../../record-view/section.js";
5
5
  import { shownImage } from "../../live/file-values.js";
6
- import { ErrorBanner } from "../error-banner.js";
7
6
  import { useAdminMeta, usePermissions } from "../meta-gate.js";
7
+ import { ErrorBanner } from "../error-banner.js";
8
8
  import { FileImage } from "../../live/file-image.js";
9
9
  import { galleryTiles } from "./gallery-layout.js";
10
10
  import { Lightbox } from "./lightbox.js";
@@ -1,9 +1,9 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { useRecord } from "../../data/use-record.js";
3
3
  import { humanize } from "../../live/naming.js";
4
+ import { AppLink } from "../use-app-address.js";
4
5
  import { SpecTable } from "../../record-view/spec-table.js";
5
6
  import { ErrorBanner } from "../error-banner.js";
6
- import { AppLink } from "../use-app-address.js";
7
7
  import { fieldAccess } from "../permissions.js";
8
8
  import { FieldValue } from "../pages/field-value.js";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -0,0 +1,7 @@
1
+ //#region src/app/group-open.ts
2
+ /** The open groups after the person opens or closes `title`. */
3
+ const toggledOpen = (open = [], title) => open.includes(title) ? open.filter((name) => name !== title) : [...open, title];
4
+ /** Whether a group shows its items: always unless `collapsed`, else when the address names it or one of its sections holds a field that needs the person's attention. */
5
+ const groupOpen = (group, open = [], needsAttention) => !group.collapsed || open.includes(group.title) || group.items.some((item) => item.kind === "section" && item.fields.some(needsAttention));
6
+ //#endregion
7
+ export { groupOpen, toggledOpen };
@@ -1,10 +1,10 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { ErrorBanner } from "../error-banner.js";
4
3
  import { recordKey, recordTitle } from "../../live/model-helpers.js";
5
4
  import { loadedFields } from "../displays/list-displays.js";
6
5
  import { useAdminMeta } from "../meta-gate.js";
7
6
  import { useAppAddress } from "../use-app-address.js";
7
+ import { ErrorBanner } from "../error-banner.js";
8
8
  import { useProjectUi } from "../pages/project-ui.js";
9
9
  import { DataGrid } from "../../data-grid/data-grid.js";
10
10
  import { embeddedColumns } from "../embedded-list/embedded-columns.js";
@@ -1,5 +1,6 @@
1
1
  import { humanize } from "../live/naming.js";
2
2
  import { createWhere, printFilter } from "@protobase/schema";
3
+ import { layoutItems } from "@protobase/schema/internal";
3
4
  //#region src/app/incoming-relations.ts
4
5
  const where$1 = createWhere();
5
6
  /**
@@ -10,7 +11,7 @@ const where$1 = createWhere();
10
11
  */
11
12
  const incomingRelations = (model, resources, views, layout, lines) => {
12
13
  if (model.primaryKey.length !== 1) return [];
13
- const listed = new Set(layout.flatMap((item) => item.kind === "related" && !item.through ? [`${item.resource}.${item.field}`] : []));
14
+ const listed = new Set(layoutItems(layout).flatMap((item) => item.kind === "related" && !item.through ? [`${item.resource}.${item.field}`] : []));
14
15
  const found = Object.values(resources).flatMap((source) => Object.values(source.fields).filter((field) => field.type === "relation" && field.relation?.resource === model.name && field.relation.columns.length === 1 && field.filterable).filter((field) => field.name !== source.tenant && source.name !== lines?.name && !listed.has(`${source.name}.${field.name}`)).map((field) => ({
15
16
  source,
16
17
  field: field.name
@@ -4,7 +4,6 @@ import { PageHeader } from "../app-shell/page-header.js";
4
4
  import { useAuth } from "../auth/auth-provider.js";
5
5
  import { Button, buttonClass } from "../primitives/button.js";
6
6
  import { humanize } from "../live/naming.js";
7
- import { ErrorBanner } from "./error-banner.js";
8
7
  import { recordKey, recordTitleField } from "../live/model-helpers.js";
9
8
  import { columnsFromModel, listColumnNames } from "../live/columns-from-model.js";
10
9
  import { displayRow, listDisplayChoices, loadedFields, shownListDisplay } from "./displays/list-displays.js";
@@ -12,6 +11,7 @@ import { parseSort, sortToOrderBy } from "./list-order.js";
12
11
  import { listedResource } from "./current-address.js";
13
12
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
14
13
  import { AppLink, useAppAddress } from "./use-app-address.js";
14
+ import { ErrorBanner } from "./error-banner.js";
15
15
  import { useProjectUi } from "./pages/project-ui.js";
16
16
  import { Sheet } from "../primitives/sheet.js";
17
17
  import { can, rowAllows } from "./permissions.js";
@@ -1,8 +1,8 @@
1
1
  import { Button } from "../../../primitives/button.js";
2
2
  import { Field } from "../../../record-view/field.js";
3
+ import { useAdminMeta } from "../../meta-gate.js";
3
4
  import { isChosen } from "../../create/create-values.js";
4
5
  import { ErrorBanner } from "../../error-banner.js";
5
- import { useAdminMeta } from "../../meta-gate.js";
6
6
  import { useProjectUi } from "../project-ui.js";
7
7
  import { FieldEditor } from "../../field-editor.js";
8
8
  import { ResponsiveDialog } from "../../../primitives/responsive-dialog.js";
@@ -1,6 +1,6 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
- import { ErrorBanner } from "../../error-banner.js";
3
2
  import { useAdminMeta } from "../../meta-gate.js";
3
+ import { ErrorBanner } from "../../error-banner.js";
4
4
  import { RecordScope } from "../record-scope.js";
5
5
  import { scopeOf, useListPage } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -4,10 +4,10 @@ import { useClient } from "../../../data/api-provider.js";
4
4
  import { humanize } from "../../../live/naming.js";
5
5
  import { useToast } from "../../../toasts/toast-provider.js";
6
6
  import { Field } from "../../../record-view/field.js";
7
+ import { useAdminMeta, usePermissions } from "../../meta-gate.js";
7
8
  import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
8
9
  import { createBody, isChosen } from "../../create/create-values.js";
9
10
  import { ErrorBanner } from "../../error-banner.js";
10
- import { useAdminMeta, usePermissions } from "../../meta-gate.js";
11
11
  import { FieldEditor } from "../../field-editor.js";
12
12
  import { fieldErrors, fieldMessage } from "../../field-errors.js";
13
13
  import { useFocusFirstInvalid } from "../../use-focus-first-invalid.js";
@@ -1,6 +1,6 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
- import { ErrorBanner } from "../../error-banner.js";
3
2
  import { useAdminMeta } from "../../meta-gate.js";
3
+ import { ErrorBanner } from "../../error-banner.js";
4
4
  import { RecordScope } from "../record-scope.js";
5
5
  import { useFirstRecord } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -1,43 +1,14 @@
1
- import { useClient } from "../../../data/api-provider.js";
2
- import { useAdminMeta } from "../../meta-gate.js";
3
1
  import { useLayoutRecord } from "../record-scope.js";
4
- import { countQuery } from "../use-layout-data.js";
5
2
  import { propsOf } from "./block-props.js";
6
- import { usePathValues } from "../use-path-value.js";
7
- import { useMemo } from "react";
8
- import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
9
- import { useQueries } from "@tanstack/react-query";
10
- import { countedResource, evaluateCondition, parseCondition } from "@protobase/layout";
3
+ import { useConditions } from "../use-conditions.js";
4
+ import { Fragment, jsx } from "react/jsx-runtime";
5
+ import "@protobase/layout";
11
6
  //#region src/app/pages/blocks/show.tsx
12
7
  /** Its children while `when` holds: fields of the record around it, and `<resource>.count` for any resource. */
13
8
  const ShowBlock = ({ node, children }) => {
14
9
  const { when } = propsOf(node);
15
- const { resources } = useAdminMeta();
16
- const client = useClient();
17
- const scope = useLayoutRecord();
18
- const condition = useMemo(() => parseCondition(when), [when]);
19
- const paths = condition.ok ? condition.paths : [];
20
- const counted = paths.map((path) => countedResource(path, (name) => Boolean(scope && Object.hasOwn(scope.model.fields, name))));
21
- const fieldPaths = paths.filter((_, index) => counted[index] === void 0);
22
- const fields = usePathValues(scope, fieldPaths);
23
- const counts = useQueries({ queries: counted.flatMap((resource) => {
24
- const model = resource === void 0 ? void 0 : resources[resource];
25
- if (!model) return [];
26
- return [countQuery(client, model)];
27
- }) });
28
- if (!condition.ok) return null;
29
- if (fields.some((value) => value.state === "loading") || counts.some((count) => count.isPending)) return null;
30
- const values = {};
31
- fieldPaths.forEach((path, index) => {
32
- const value = fields[index];
33
- if (value.state === "ready") values[path] = value.value;
34
- });
35
- let next = 0;
36
- paths.forEach((path, index) => {
37
- if (counted[index] === void 0) return;
38
- values[path] = resources[counted[index]] ? counts[next++]?.data ?? 0 : 0;
39
- });
40
- return evaluateCondition(condition, values) ? /* @__PURE__ */ jsx(Fragment$1, { children }) : null;
10
+ const [shown] = useConditions([when], useLayoutRecord());
11
+ return shown ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
41
12
  };
42
13
  //#endregion
43
14
  export { ShowBlock };
@@ -1,8 +1,8 @@
1
1
  import { useTopBarSlot } from "../../app-shell/top-bar-slot.js";
2
2
  import "../../app-shell/top-bar.js";
3
3
  import { ActionRail } from "../../record-view/action-rail.js";
4
- import { ActionMenu } from "../../record-view/action-menu.js";
5
4
  import { AppLink, useAppAddress } from "../use-app-address.js";
5
+ import { ActionMenu } from "../../record-view/action-menu.js";
6
6
  import { ActionPrompt, asksFirst } from "./action-prompt.js";
7
7
  import { useActionRunner } from "./use-run-action.js";
8
8
  import { actionIcon } from "../action-icon.js";
@@ -0,0 +1,49 @@
1
+ import { useClient } from "../../data/api-provider.js";
2
+ import { useAdminMeta } from "../meta-gate.js";
3
+ import { countQuery } from "./use-layout-data.js";
4
+ import { usePathValues } from "./use-path-value.js";
5
+ import { useMemo } from "react";
6
+ import { useQueries } from "@tanstack/react-query";
7
+ import { countedResource, evaluateCondition, parseCondition } from "@protobase/layout";
8
+ //#region src/app/pages/use-conditions.ts
9
+ /**
10
+ * Whether each condition holds for the record around it (`scope`): `undefined` while a value it reads loads, `false`
11
+ * when it does not parse. Fields come from the stored record, followed through relations; `<resource>.count` counts
12
+ * records. All conditions share one set of queries, so the hook count never changes with the conditions.
13
+ */
14
+ const useConditions = (whens, scope) => {
15
+ const { resources } = useAdminMeta();
16
+ const client = useClient();
17
+ const key = JSON.stringify(whens);
18
+ const parsed = useMemo(() => JSON.parse(key).map(parseCondition), [key]);
19
+ const isField = (name) => Boolean(scope && Object.hasOwn(scope.model.fields, name));
20
+ const paths = [...new Set(parsed.flatMap((condition) => condition.ok ? condition.paths : []))];
21
+ const counted = new Map(paths.flatMap((path) => {
22
+ const resource = countedResource(path, isField);
23
+ return resource === void 0 ? [] : [[path, resource]];
24
+ }));
25
+ const fieldPaths = paths.filter((path) => !counted.has(path));
26
+ const countedModels = [...new Set(counted.values())].flatMap((resource) => resources[resource] ? [resources[resource]] : []);
27
+ const fields = usePathValues(scope, fieldPaths);
28
+ const counts = useQueries({ queries: countedModels.map((model) => countQuery(client, model)) });
29
+ return parsed.map((condition) => {
30
+ if (!condition.ok) return false;
31
+ const values = {};
32
+ for (const path of condition.paths) {
33
+ const resource = counted.get(path);
34
+ if (resource === void 0) {
35
+ const value = fields[fieldPaths.indexOf(path)];
36
+ if (value.state === "loading") return void 0;
37
+ if (value.state === "ready") values[path] = value.value;
38
+ continue;
39
+ }
40
+ const index = countedModels.findIndex((model) => model.name === resource);
41
+ if (index === -1) values[path] = 0;
42
+ else if (counts[index].isPending) return void 0;
43
+ else values[path] = counts[index].data ?? 0;
44
+ }
45
+ return evaluateCondition(condition, values);
46
+ });
47
+ };
48
+ //#endregion
49
+ export { useConditions };
@@ -32,7 +32,10 @@ const recordShown = (mode, canUpdate, edit) => {
32
32
  const helpShown = (own, view, editing) => (own ?? view?.helpWhen ?? "always") === "always" || editing;
33
33
  /** A field's help line on the record page, when it shows. */
34
34
  const fieldHelp = (hints, view, editing) => hints?.help !== void 0 && helpShown(hints.helpWhen, view, editing) ? hints.help : void 0;
35
- /** Whether a field or layout item shows: always, unless it says only while editing or only while reading and the page is not. */
35
+ /**
36
+ * Whether a field or layout item shows: always, unless it says only while editing or only while reading and the page is not.
37
+ * A record condition is no mode and counts as always here; the record page evaluates it separately.
38
+ */
36
39
  const whenShown = (when, editing) => {
37
40
  if (when === "editing") return editing;
38
41
  if (when === "reading") return !editing;
@@ -0,0 +1,25 @@
1
+ import { whenShown } from "./record-mode.js";
2
+ import { isRecordCondition } from "@protobase/schema";
3
+ //#region src/app/record-mode/shown-layout.ts
4
+ /** The items with a record condition in `when`, those inside groups too, whose conditions the page evaluates: a group before its items. */
5
+ const conditionalItems = (items) => items.flatMap((item) => item.kind === "group" ? [item, ...item.items] : [item]).filter((item) => isRecordCondition(item.when));
6
+ /**
7
+ * The items that show: those with a record condition only when it holds (`held`, one answer per conditional item in
8
+ * the order `conditionalItems` lists them, `undefined` while loading) and the page is no preview; the others by the
9
+ * form or read view. A group keeps the items inside it that show, and goes when it or all of them do not.
10
+ */
11
+ const shownLayout = (items, held, editing, preview) => {
12
+ let next = 0;
13
+ const shown = (item) => isRecordCondition(item.when) ? !preview && held[next++] === true : whenShown(item.when, editing);
14
+ return items.flatMap((item) => {
15
+ if (item.kind !== "group") return shown(item) ? [item] : [];
16
+ const groupShown = shown(item);
17
+ const inner = item.items.filter(shown);
18
+ return groupShown && inner.length > 0 ? [{
19
+ ...item,
20
+ items: inner
21
+ }] : [];
22
+ });
23
+ };
24
+ //#endregion
25
+ export { conditionalItems, shownLayout };
@@ -0,0 +1,14 @@
1
+ import { useConditions } from "../pages/use-conditions.js";
2
+ import { conditionalItems, shownLayout } from "./shown-layout.js";
3
+ //#region src/app/record-mode/use-shown-layout.ts
4
+ /**
5
+ * The layout items to draw: those meant for the form or the read view as the page is, and those with a record
6
+ * condition while it holds for the saved record. Conditional items stay out while their values load and on a preview.
7
+ */
8
+ const useShownLayout = (items, scope, editing, preview) => {
9
+ const conditions = preview ? [] : conditionalItems(items).map((item) => item.when);
10
+ const held = useConditions(conditions, scope);
11
+ return shownLayout(items, held, editing, preview);
12
+ };
13
+ //#endregion
14
+ export { useShownLayout };