@protobase/schema 0.21.11 → 0.22.0

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.
@@ -23,6 +23,8 @@ export declare const addressWords: {
23
23
  readonly create: "new";
24
24
  /** A record's form, `edit=true`, on a view whose record page opens as a read view or locked. */
25
25
  readonly edit: "edit";
26
+ /** A record page's open collapsed group, by title, once per group. */
27
+ readonly open: "open";
26
28
  };
27
29
  /** The path of `address` below `basePath` (such as `/admin`), every name and key encoded the one way the app reads them. */
28
30
  export declare const appPath: (address: AppAddress, basePath?: string) => string;
@@ -22,7 +22,9 @@ const addressWords = {
22
22
  /** The second path segment that opens a resource's create form instead of a record. */
23
23
  create: "new",
24
24
  /** A record's form, `edit=true`, on a view whose record page opens as a read view or locked. */
25
- edit: "edit"
25
+ edit: "edit",
26
+ /** A record page's open collapsed group, by title, once per group. */
27
+ open: "open"
26
28
  };
27
29
  const listWords = [
28
30
  "display",
@@ -71,12 +73,19 @@ const listQuery = (address) => {
71
73
  const text = query.toString();
72
74
  return text ? `?${text}` : "";
73
75
  };
76
+ const recordQuery = (address) => {
77
+ const query = new URLSearchParams();
78
+ if (address.edit) query.set(addressWords.edit, "true");
79
+ for (const title of address.open ?? []) query.append(addressWords.open, title);
80
+ const text = query.toString();
81
+ return text ? `?${text}` : "";
82
+ };
74
83
  /** The path of `address` below `basePath` (such as `/admin`), every name and key encoded the one way the app reads them. */
75
84
  const appPath = (address, basePath = "") => {
76
85
  switch (address.kind) {
77
86
  case "page": return `${basePath}/${encodeURIComponent(address.page)}`;
78
87
  case "create": return `${basePath}/${encodeURIComponent(address.resource)}/${addressWords.create}${valuesQuery(address.values)}`;
79
- case "record": return `${basePath}/${encodeURIComponent(address.resource)}/${encodeURIComponent(encodeKey(address.key))}${address.edit ? `?${addressWords.edit}=true` : ""}`;
88
+ case "record": return `${basePath}/${encodeURIComponent(address.resource)}/${encodeURIComponent(encodeKey(address.key))}${recordQuery(address)}`;
80
89
  case "list": return `${basePath}/${encodeURIComponent(address.resource)}${listQuery(address)}`;
81
90
  }
82
91
  };
@@ -115,12 +124,16 @@ const readAppPath = (pathAndQuery, pages) => {
115
124
  ...Object.keys(values).length > 0 && { values }
116
125
  };
117
126
  }
118
- if (second !== void 0) return {
119
- kind: "record",
120
- resource: first,
121
- key: second,
122
- ...url.searchParams.get(addressWords.edit) === "true" && { edit: true }
123
- };
127
+ if (second !== void 0) {
128
+ const open = [...new Set(url.searchParams.getAll(addressWords.open).filter((title) => title !== ""))];
129
+ return {
130
+ kind: "record",
131
+ resource: first,
132
+ key: second,
133
+ ...url.searchParams.get(addressWords.edit) === "true" && { edit: true },
134
+ ...open.length > 0 && { open }
135
+ };
136
+ }
124
137
  const params = url.searchParams;
125
138
  const display = params.get(addressWords.display) || void 0;
126
139
  const filter = params.get(addressWords.filter) || void 0;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FileOptions, FileProcessor, FileProcessorInput, ImageFormat, ImageVariant, ImageVariantOptions, imageFormats } from "./files/file-options.js";
2
- import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CalendarView, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CommentsSectionModel, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, FormModel, GalleryModel, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RecordWhen, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
2
+ import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CalendarView, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CommentsSectionModel, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, FormModel, GalleryModel, GroupChartModel, GroupItemModel, GroupModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RecordWhen, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
3
3
  import { AccessAction, AccessContext, AccessFn, AccessResult, AccessUser, FieldAccess, FieldRule } from "./access/types.js";
4
4
  import { AnyField, Constraint, Field, FieldMeta, FieldTypeDef } from "./field.js";
5
5
  import { RelationField } from "./relation-field.js";
@@ -42,4 +42,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
42
42
  import { restrictModel } from "./access/restrict.js";
43
43
  import "./access/index.js";
44
44
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
45
- export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type CalendarView, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CommentsSectionModel, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type FormAddress, type FormInput, type FormModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type RecordWhen, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, isRecordCondition, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
45
+ export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type CalendarView, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CommentsSectionModel, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type FormAddress, type FormInput, type FormModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type GroupItemModel, type GroupModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type RecordWhen, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, isRecordCondition, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
@@ -17,4 +17,5 @@ import { promptError, promptFields, renderPrompt } from "./ai-prompt.js";
17
17
  import { AddressCheck, addressErrors } from "./address-errors.js";
18
18
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
19
19
  import { readEventStream } from "./assistant/event-stream.js";
20
- export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
20
+ import { layoutItems } from "./layout-items.js";
21
+ export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, layoutItems, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/internal.js CHANGED
@@ -17,4 +17,5 @@ import { redactSecrets } from "./redact-secrets.js";
17
17
  import { addressErrors } from "./address-errors.js";
18
18
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
19
19
  import { readEventStream } from "./assistant/event-stream.js";
20
- export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
20
+ import { layoutItems } from "./layout-items.js";
21
+ export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, layoutItems, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
@@ -0,0 +1,5 @@
1
+ import { LayoutItemModel } from "./model.js";
2
+ //#region src/layout-items.d.ts
3
+ /** The layout's items with every group's items in its place, for code that looks for sections and related tables wherever they sit. */
4
+ export declare const layoutItems: (layout: readonly LayoutItemModel[]) => LayoutItemModel[];
5
+ //#endregion
@@ -0,0 +1,5 @@
1
+ //#region src/layout-items.ts
2
+ /** The layout's items with every group's items in its place, for code that looks for sections and related tables wherever they sit. */
3
+ const layoutItems = (layout) => layout.flatMap((item) => item.kind === "group" ? item.items : [item]);
4
+ //#endregion
5
+ export { layoutItems };
package/dist/layout.d.ts CHANGED
@@ -50,6 +50,15 @@ export declare const layout: {
50
50
  sidebar: (fields: Ref[], o?: {
51
51
  when?: RecordWhen;
52
52
  }) => LayoutItemModel;
53
+ /**
54
+ * A heading over sections and related tables in the main column; `collapsed: true` keeps it closed until the person
55
+ * opens it, or a link to the record does (the address names the open groups by title). Throws on any other item. See
56
+ * [Groups](website/src/content/docs/reference/ui-config.mdx#groups).
57
+ */
58
+ group: (title: string, items: LayoutItemModel[], o?: {
59
+ collapsed?: boolean;
60
+ when?: RecordWhen;
61
+ }) => LayoutItemModel;
53
62
  /** A table of the records of another resource that point at this one, directly or through a join table. */
54
63
  related: (title: string, o: Omit<RelatedModel, "kind" | "title">) => LayoutItemModel;
55
64
  /**
package/dist/layout.js CHANGED
@@ -59,6 +59,22 @@ const layout = {
59
59
  fields: fields.map((ref) => ref.name),
60
60
  ...o.when !== void 0 && { when: o.when }
61
61
  }),
62
+ /**
63
+ * A heading over sections and related tables in the main column; `collapsed: true` keeps it closed until the person
64
+ * opens it, or a link to the record does (the address names the open groups by title). Throws on any other item. See
65
+ * [Groups](website/src/content/docs/reference/ui-config.mdx#groups).
66
+ */
67
+ group: (title, items, o = {}) => {
68
+ const wrong = items.find((item) => item.kind !== "section" && item.kind !== "related");
69
+ if (wrong) throw new Error(`l.group "${title}" holds sections and related tables, not ${wrong.kind}`);
70
+ return {
71
+ kind: "group",
72
+ title,
73
+ items,
74
+ ...o.collapsed && { collapsed: true },
75
+ ...o.when !== void 0 && { when: o.when }
76
+ };
77
+ },
62
78
  /** A table of the records of another resource that point at this one, directly or through a join table. */
63
79
  related: (title, o) => ({
64
80
  kind: "related",
package/dist/model.d.ts CHANGED
@@ -218,6 +218,8 @@ export type AppAddress = {
218
218
  * Edit opens. A view whose record page is always editable ignores it.
219
219
  */
220
220
  edit?: boolean;
221
+ /** The titles of the record page's collapsed groups that are open. */
222
+ open?: readonly string[];
221
223
  } | {
222
224
  /** The form for a new record. */
223
225
  kind: 'create';
@@ -714,8 +716,8 @@ export type CommentsSectionModel = {
714
716
  };
715
717
  /** How a section shows its fields on the read view: `facts` as a row of key facts; label/value rows when omitted. */
716
718
  export type SectionPresentation = 'facts';
717
- /** An item of the record page (`.layout((r) => ...)` with `l`); see [The record page](website/src/content/docs/reference/ui-config.mdx#the-record-page). */
718
- export type LayoutItemModel = {
719
+ /** A titled group of fields on the record page; see [The record page](website/src/content/docs/reference/ui-config.mdx#the-record-page). */
720
+ export type SectionModel = {
719
721
  /** A titled group of fields in the main column, editable where the user may write. */
720
722
  kind: 'section';
721
723
  /** The section's title. */
@@ -733,14 +735,31 @@ export type LayoutItemModel = {
733
735
  presentation?: SectionPresentation;
734
736
  /** Whether it shows on the form, the read view or both, or a condition on the saved record that it shows while it holds. */
735
737
  when?: RecordWhen;
736
- } | {
738
+ };
739
+ /** An item of the record page (`.layout((r) => ...)` with `l`); see [The record page](website/src/content/docs/reference/ui-config.mdx#the-record-page). */
740
+ export type LayoutItemModel = SectionModel | {
737
741
  /** A read-only summary card beside the main column. */
738
742
  kind: 'sidebar';
739
743
  /** The fields it shows, in order. */
740
744
  fields: string[];
741
745
  /** Whether it shows on the form, the read view or both, or a condition on the saved record that it shows while it holds. */
742
746
  when?: RecordWhen;
743
- } | RelatedModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel | CommentsSectionModel;
747
+ } | RelatedModel | GroupModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel | CommentsSectionModel;
748
+ /** A titled group of sections and related tables on the record page, closed until opened with `collapsed`; see [Groups](website/src/content/docs/reference/ui-config.mdx#groups). */
749
+ export type GroupModel = {
750
+ /** A group of sections and related tables. */
751
+ kind: 'group';
752
+ /** Its heading, unique among the view's groups: the address names an open group by it. */
753
+ title: string;
754
+ /** The sections and related tables it holds, in order. */
755
+ items: GroupItemModel[];
756
+ /** Closed until the person opens it, or a link does. */
757
+ collapsed?: boolean;
758
+ /** Whether it shows on the form, the read view or both. */
759
+ when?: RecordWhen;
760
+ };
761
+ /** An item a group may hold. */
762
+ export type GroupItemModel = SectionModel | RelatedModel;
744
763
  /**
745
764
  * A record's photos as tiles in the main column under `title`, one large and the next few smaller, opening a lightbox
746
765
  * that steps through them all: first the record's own file field `image`, then the `image` of the records of
package/dist/view.js CHANGED
@@ -260,6 +260,12 @@ var ViewBuilder = class ViewBuilder {
260
260
  }
261
261
  const clash = resource ? list?.relatedColumns?.find((column) => Object.hasOwn(resource.fields, column.name)) : void 0;
262
262
  if (clash) throw new Error(`View "${this.state.resource}": related column "${clash.name}" has the name of a field of the resource`);
263
+ const groupTitles = /* @__PURE__ */ new Set();
264
+ for (const item of this.state.layout) {
265
+ if (item.kind !== "group") continue;
266
+ if (groupTitles.has(item.title)) throw new Error(`View "${this.state.resource}": two groups are titled "${item.title}"`);
267
+ groupTitles.add(item.title);
268
+ }
263
269
  for (const item of resource ? this.state.layout : []) {
264
270
  if (item.kind !== "gallery" || item.image === void 0) continue;
265
271
  if (!Object.hasOwn(resource.fields, item.image)) throw new Error(`View "${this.state.resource}": gallery "${item.title}" names field "${item.image}", which the resource does not have`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.21.11",
3
+ "version": "0.22.0",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {