@protobase/ui 0.21.11 → 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 (51) 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 +2 -1
  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/record-actions.js +1 -1
  25. package/dist/app/record-mode/shown-layout.js +13 -6
  26. package/dist/app/record-page.js +81 -64
  27. package/dist/app/record-row/record-row-body.js +1 -1
  28. package/dist/app/record-row/saved-record-row.js +1 -1
  29. package/dist/app/related-section.js +1 -1
  30. package/dist/app/searches/saved-search-rows.js +1 -1
  31. package/dist/app/searches/saved-searches.js +1 -1
  32. package/dist/app/sensitive-field.js +1 -1
  33. package/dist/app/use-app-address.d.ts +1 -0
  34. package/dist/app/use-record-editor.js +1 -1
  35. package/dist/forms/public-form-page.js +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/live/field-values.js +1 -1
  38. package/dist/package.js +1 -1
  39. package/dist/record-view/group-section.js +35 -0
  40. package/dist/shell/assets/app-IuP56Dep.js +69 -0
  41. package/dist/shell/assets/{index-CzqIlZPP.css → index-DhW8t8wq.css} +1 -1
  42. package/dist/shell/assets/{index-Dimu63Ym.js → index-GB5YRjlo.js} +1 -1
  43. package/dist/shell/assets/{maplibre-ERswanhA.js → maplibre-BLm41BrA.js} +1 -1
  44. package/dist/shell/assets/{markdown-content-D-haiVrI.js → markdown-content-Cia4jbSB.js} +1 -1
  45. package/dist/shell/assets/{record-map-C3i03kwc.js → record-map-C-Svn7nc.js} +1 -1
  46. package/dist/shell/assets/{records-map-C1JyIuz3.js → records-map-D5LZhC54.js} +1 -1
  47. package/dist/shell/assets/{shared-protobase-ui-Bt4hh6GB.js → shared-protobase-ui-D16YbnPr.js} +1 -1
  48. package/dist/shell/index.html +4 -4
  49. package/dist/shell/protobase.shell.json +1 -1
  50. package/package.json +4 -4
  51. package/dist/shell/assets/app-CiQapuHq.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,6 +1,7 @@
1
1
  import { isEditable, toPatchValue } from "../../live/field-values.js";
2
2
  import { whenShown } from "../record-mode/record-mode.js";
3
3
  import { isRecordCondition } from "@protobase/schema";
4
+ import { layoutItems } from "@protobase/schema/internal";
4
5
  //#region src/app/create/create-values.ts
5
6
  const isChosen = (value) => typeof value === "object" && value !== null && "key" in value;
6
7
  /** Fields the user can set on a new record: not read-only, not the tenant, and not structured data. */
@@ -13,7 +14,7 @@ const isCreatable = (model, field) => field.name !== model.tenant && !field.read
13
14
  const createSections = (model, view) => {
14
15
  const creatable = Object.values(model.fields).filter((field) => isCreatable(model, field)).map((field) => field.name);
15
16
  const placed = /* @__PURE__ */ new Set();
16
- const sections = (view?.layout ?? []).flatMap((item) => {
17
+ const sections = layoutItems(view?.layout ?? []).flatMap((item) => {
17
18
  if (item.kind !== "section" || isRecordCondition(item.when) || !whenShown(item.when, true)) return [];
18
19
  const fields = item.fields.filter((name) => creatable.includes(name) && !placed.has(name) && whenShown(view?.fields[name]?.when, true));
19
20
  fields.forEach((name) => placed.add(name));
@@ -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,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";
@@ -1,17 +1,24 @@
1
1
  import { whenShown } from "./record-mode.js";
2
2
  import { isRecordCondition } from "@protobase/schema";
3
3
  //#region src/app/record-mode/shown-layout.ts
4
- /** The items with a record condition in `when`, whose conditions the page evaluates. */
5
- const conditionalItems = (items) => items.filter((item) => isRecordCondition(item.when));
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
6
  /**
7
7
  * The items that show: those with a record condition only when it holds (`held`, one answer per conditional item in
8
- * order, `undefined` while loading) and the page is no preview; the others by the form or read view.
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.
9
10
  */
10
11
  const shownLayout = (items, held, editing, preview) => {
11
12
  let next = 0;
12
- return items.filter((item) => {
13
- if (!isRecordCondition(item.when)) return whenShown(item.when, editing);
14
- return !preview && held[next++] === true;
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
+ }] : [];
15
22
  });
16
23
  };
17
24
  //#endregion
@@ -13,12 +13,6 @@ import { Field } from "../record-view/field.js";
13
13
  import { Section } from "../record-view/section.js";
14
14
  import { MetadataCard } from "../record-view/record-sidebar.js";
15
15
  import { RecordHeader } from "../record-view/record-header.js";
16
- import { SpecTable } from "../record-view/spec-table.js";
17
- import { KeyFacts } from "../record-view/key-facts.js";
18
- import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
19
- import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
20
- import { isChosen, keyValue } from "./create/create-values.js";
21
- import { ErrorBanner } from "./error-banner.js";
22
16
  import { isMarkdown } from "../markdown/markdown-hint.js";
23
17
  import { recordTitle } from "../live/model-helpers.js";
24
18
  import { contactFormat } from "../live/contact-links.js";
@@ -27,6 +21,13 @@ import { displayValue, valueTitle } from "../live/display-value.js";
27
21
  import { filledFields } from "./filled-fields.js";
28
22
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
29
23
  import { AppLink, useAppAddress } from "./use-app-address.js";
24
+ import { GroupSection } from "../record-view/group-section.js";
25
+ import { SpecTable } from "../record-view/spec-table.js";
26
+ import { KeyFacts } from "../record-view/key-facts.js";
27
+ import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
28
+ import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
29
+ import { isChosen, keyValue } from "./create/create-values.js";
30
+ import { ErrorBanner } from "./error-banner.js";
30
31
  import { useProjectUi } from "./pages/project-ui.js";
31
32
  import { shownWidget } from "./field-types/shown-widget.js";
32
33
  import { WidgetPart } from "./field-types/widget-part.js";
@@ -45,6 +46,7 @@ import { auditField } from "../live/audit-fields.js";
45
46
  import { CustomSection } from "./custom-section.js";
46
47
  import { MapSection } from "./map/map-section.js";
47
48
  import { RecordActions } from "./pages/record-actions.js";
49
+ import { groupOpen, toggledOpen } from "./group-open.js";
48
50
  import { RecordLines, findLinesResource } from "./record-lines.js";
49
51
  import { draftStorageKey } from "./draft-storage.js";
50
52
  import { useLines } from "./use-lines.js";
@@ -150,15 +152,17 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
150
152
  const { shown, offersEdit, editing } = recordShown(view?.recordMode, canUpdate, address?.kind === "record" && address.edit === true);
151
153
  useNavigationGuard(editor.dirty && shown === "form");
152
154
  usePauseFocusRefresh(editor.dirty);
153
- const readAddress = {
155
+ const openGroups = address?.kind === "record" ? address.open : void 0;
156
+ const atEdit = address?.kind === "record" && address.edit === true;
157
+ const recordAt = (open = [], edit = false) => ({
154
158
  kind: "record",
155
159
  resource: model.name,
156
- key: recordKey
157
- };
158
- const editAddress = {
159
- ...readAddress,
160
- edit: true
161
- };
160
+ key: recordKey,
161
+ ...edit && { edit },
162
+ ...open.length > 0 && { open }
163
+ });
164
+ const readAddress = recordAt(openGroups);
165
+ const editAddress = recordAt(openGroups, true);
162
166
  const leavesForm = shown === "form" && view?.recordMode !== void 0 && view.recordMode !== "edit";
163
167
  const canDelete = can(permissions, "delete") && stored.permissions?.delete !== false;
164
168
  const remove = useDeleteFlow({
@@ -344,6 +348,69 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
344
348
  children: "Unsaved changes"
345
349
  })
346
350
  ] });
351
+ const mainItem = (item, index) => {
352
+ if (item.kind === "group") {
353
+ const children = item.items.flatMap(mainItem);
354
+ if (children.length === 0) return [];
355
+ const open = groupOpen(item, openGroups, (name) => editing && (editor.errors[name] !== void 0 || editor.conflicts[name] !== void 0));
356
+ const toggle = item.collapsed ? recordAt(toggledOpen(openGroups, item.title), atEdit) : void 0;
357
+ return [/* @__PURE__ */ jsx(GroupSection, {
358
+ title: item.title,
359
+ open,
360
+ toggle,
361
+ children
362
+ }, `group:${item.title}`)];
363
+ }
364
+ if (item.kind === "related") return [/* @__PURE__ */ jsx(RelatedSection, {
365
+ item,
366
+ recordKey,
367
+ owner: model.name
368
+ }, `related:${item.title}`)];
369
+ if (preview && (item.kind === "custom" || item.kind === "map" || item.kind === "gallery")) return [];
370
+ if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
371
+ item,
372
+ scope
373
+ }, `custom:${index}`)];
374
+ if (item.kind === "map") return item.sidebar ? [] : [/* @__PURE__ */ jsx(MapSection, {
375
+ item,
376
+ record: stored.record
377
+ }, `map:${index}`)];
378
+ if (item.kind === "gallery") return [/* @__PURE__ */ jsx(GallerySection, {
379
+ item,
380
+ record: stored.record,
381
+ recordKey
382
+ }, `gallery:${index}`)];
383
+ if (item.kind === "history") return [/* @__PURE__ */ jsx(HistorySection, {
384
+ item,
385
+ model,
386
+ view,
387
+ recordKey,
388
+ etag: stored.etag
389
+ }, `history:${index}`)];
390
+ if (item.kind === "comments") return [/* @__PURE__ */ jsx(CommentsSection, {
391
+ item,
392
+ resource: model.name,
393
+ recordKey,
394
+ canWrite: auth.kind !== "visitor"
395
+ }, `comments:${index}`)];
396
+ if (item.kind !== "section") return [];
397
+ const visibleFields = item.fields.filter((name) => model.fields[name] && visible(name));
398
+ const facts = shown === "read" && item.presentation === "facts";
399
+ const fields = facts ? filledFields(stored.record, visibleFields) : visibleFields;
400
+ if (fields.length === 0) return [];
401
+ return [/* @__PURE__ */ jsx(Section, {
402
+ title: item.title,
403
+ help: helpShown(item.helpWhen, view, editing) ? item.help : void 0,
404
+ children: facts ? /* @__PURE__ */ jsx(KeyFacts, { facts: fields.map((name) => ({
405
+ label: readRow(name).label,
406
+ value: readRow(name).value,
407
+ icon: namedIcon(hint(name)?.icon)
408
+ })) }) : shown === "read" ? /* @__PURE__ */ jsx(SpecTable, { rows: fields.map(readRow) }) : /* @__PURE__ */ jsx("div", {
409
+ className: "grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2",
410
+ children: fields.map(fieldView)
411
+ })
412
+ }, item.title)];
413
+ };
347
414
  return /* @__PURE__ */ jsxs("div", {
348
415
  ref: fieldsRef,
349
416
  className: "flex flex-1 flex-col",
@@ -412,57 +479,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
412
479
  className: "grid grid-cols-1 gap-8 md:grid-cols-[minmax(0,1fr)_300px] md:gap-12",
413
480
  children: [/* @__PURE__ */ jsxs("main", {
414
481
  className: "divide-y divide-border",
415
- children: [layout.flatMap((item, index) => {
416
- if (item.kind === "related") return [/* @__PURE__ */ jsx(RelatedSection, {
417
- item,
418
- recordKey,
419
- owner: model.name
420
- }, `related:${item.title}`)];
421
- if (preview && (item.kind === "custom" || item.kind === "map" || item.kind === "gallery")) return [];
422
- if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
423
- item,
424
- scope
425
- }, `custom:${index}`)];
426
- if (item.kind === "map") return item.sidebar ? [] : [/* @__PURE__ */ jsx(MapSection, {
427
- item,
428
- record: stored.record
429
- }, `map:${index}`)];
430
- if (item.kind === "gallery") return [/* @__PURE__ */ jsx(GallerySection, {
431
- item,
432
- record: stored.record,
433
- recordKey
434
- }, `gallery:${index}`)];
435
- if (item.kind === "history") return [/* @__PURE__ */ jsx(HistorySection, {
436
- item,
437
- model,
438
- view,
439
- recordKey,
440
- etag: stored.etag
441
- }, `history:${index}`)];
442
- if (item.kind === "comments") return [/* @__PURE__ */ jsx(CommentsSection, {
443
- item,
444
- resource: model.name,
445
- recordKey,
446
- canWrite: auth.kind !== "visitor"
447
- }, `comments:${index}`)];
448
- if (item.kind !== "section") return [];
449
- const visibleFields = item.fields.filter((name) => model.fields[name] && visible(name));
450
- const facts = shown === "read" && item.presentation === "facts";
451
- const fields = facts ? filledFields(stored.record, visibleFields) : visibleFields;
452
- if (fields.length === 0) return [];
453
- return [/* @__PURE__ */ jsx(Section, {
454
- title: item.title,
455
- help: helpShown(item.helpWhen, view, editing) ? item.help : void 0,
456
- children: facts ? /* @__PURE__ */ jsx(KeyFacts, { facts: fields.map((name) => ({
457
- label: readRow(name).label,
458
- value: readRow(name).value,
459
- icon: namedIcon(hint(name)?.icon)
460
- })) }) : shown === "read" ? /* @__PURE__ */ jsx(SpecTable, { rows: fields.map(readRow) }) : /* @__PURE__ */ jsx("div", {
461
- className: "grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2",
462
- children: fields.map(fieldView)
463
- })
464
- }, item.title)];
465
- }), linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
482
+ children: [layout.flatMap(mainItem), linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
466
483
  lines: linesModel,
467
484
  draft: linesDraft,
468
485
  vatRate: Number(stored.record.vatRate ?? 21) / 100,
@@ -1,11 +1,11 @@
1
1
  import { 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 { displaysOf, loadedFields } from "../displays/list-displays.js";
6
5
  import { defaultOrderBy } from "../list-order.js";
7
6
  import { useAdminMeta } from "../meta-gate.js";
8
7
  import { AppLink } from "../use-app-address.js";
8
+ import { ErrorBanner } from "../error-banner.js";
9
9
  import { useLiveRows } from "../../live/use-live-rows.js";
10
10
  import { viewAllAddress } from "../embedded-list/embedded-display.js";
11
11
  import { FilterChipList } from "../searches/filter-chip-list.js";
@@ -1,8 +1,8 @@
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 { listedResource } from "../current-address.js";
5
4
  import { useAdminMeta } from "../meta-gate.js";
5
+ import { ErrorBanner } from "../error-banner.js";
6
6
  import { savedScope } from "../saved/saved-records.js";
7
7
  import { useSavedRecords } from "../saved/use-saved-records.js";
8
8
  import { RecordRowBody } from "./record-row-body.js";
@@ -2,9 +2,9 @@ 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
4
  import { Section } from "../record-view/section.js";
5
- import { ErrorBanner } from "./error-banner.js";
6
5
  import { listColumnNames } from "../live/columns-from-model.js";
7
6
  import { useAdminMeta, usePermissions } from "./meta-gate.js";
7
+ import { ErrorBanner } from "./error-banner.js";
8
8
  import { countQuery } from "./pages/use-layout-data.js";
9
9
  import { searchAll } from "../live/search-all.js";
10
10
  import { relatedOpens } from "./related-opens.js";
@@ -1,9 +1,9 @@
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 } from "../../live/model-helpers.js";
5
4
  import { useAdminMeta } from "../meta-gate.js";
6
5
  import { AppLink } from "../use-app-address.js";
6
+ import { ErrorBanner } from "../error-banner.js";
7
7
  import { InlineFilterChips } from "./filter-chip-list.js";
8
8
  import { RecordRowBody } from "../record-row/record-row-body.js";
9
9
  import { useSavedSearches } from "./use-saved-searches.js";
@@ -1,10 +1,10 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { PageHeader } from "../../app-shell/page-header.js";
3
3
  import { humanize } from "../../live/naming.js";
4
- import { ErrorBanner } from "../error-banner.js";
5
4
  import { recordKey } from "../../live/model-helpers.js";
6
5
  import { Notice, useAdminMeta, 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 } from "../permissions.js";
9
9
  import { useSavedSearches } from "./use-saved-searches.js";
10
10
  import { useDeleteFlow } from "../delete/use-delete-flow.js";
@@ -2,9 +2,9 @@ import { cn } from "../lib/cn.js";
2
2
  import { Input } from "../primitives/input.js";
3
3
  import { Button } from "../primitives/button.js";
4
4
  import { humanize } from "../live/naming.js";
5
+ import { displayValue } from "../live/display-value.js";
5
6
  import { toDraft } from "../live/field-values.js";
6
7
  import { ErrorBanner } from "./error-banner.js";
7
- import { displayValue } from "../live/display-value.js";
8
8
  import { useState } from "react";
9
9
  import { Eye, EyeOff } from "lucide-react";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -21,6 +21,7 @@ export declare const useAppAddress: () => {
21
21
  /** A link to an address in the app: an ordinary anchor that the router follows without reloading the page. */
22
22
  export declare const AppLink: ({ to, ...rest }: AnchorHTMLAttributes<HTMLAnchorElement> & {
23
23
  to: AppAddress;
24
+ replace?: boolean;
24
25
  }) => import("react").JSX.Element;
25
26
  //#endregion
26
27
  export type { CurrentAddress, ListAddress, ListChange };
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../data/query-keys.js";
2
2
  import { useClient } from "../data/api-provider.js";
3
- import { differs, toDraft, toPatchValue } from "../live/field-values.js";
4
3
  import { displayValue } from "../live/display-value.js";
4
+ import { differs, toDraft, toPatchValue } from "../live/field-values.js";
5
5
  import { fieldErrors } from "./field-errors.js";
6
6
  import { useLiveSave } from "./use-live-save.js";
7
7
  import { readDraft, writeDraft } from "./draft-storage.js";