@protobase/schema 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.
@@ -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, 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";
@@ -9,6 +9,7 @@ import { FieldRefs, FieldsInput, Ref, Refs } from "./refs.js";
9
9
  import { CreatedOptions, DeletedOptions, EventActor, EventContext, EventHelpers, EventMatch, EventSpec, UndeletedOptions, UpdatedOptions } from "./events/event-helpers.js";
10
10
  import { Access, InferRecord, Issue, KeyOf, KeyRefs, RecordKey, RecordOf } from "./resource-types.js";
11
11
  import { ResourceBuilder, SearchRef, SearchRefs, Validator, resource } from "./resource.js";
12
+ import { isRecordCondition } from "./record-when.js";
12
13
  import { ChartInput, GroupChartInput, PeriodChartInput } from "./view-chart.js";
13
14
  import { FormInput } from "./view-form.js";
14
15
  import { ListColumn } from "./view-columns.js";
@@ -41,4 +42,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
41
42
  import { restrictModel } from "./access/restrict.js";
42
43
  import "./access/index.js";
43
44
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
44
- 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 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, 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 };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { createWhere, where } from "./filter/where.js";
9
9
  import { anonymousRole } from "./access/role-entry.js";
10
10
  import { accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
11
11
  import { resource } from "./resource.js";
12
+ import { isRecordCondition } from "./record-when.js";
12
13
  import { formPath, formRoute, readFormAddress } from "./form-address.js";
13
14
  import { fieldRef } from "./field-ref.js";
14
15
  import { view } from "./view.js";
@@ -24,4 +25,4 @@ import { rule } from "./access/rule.js";
24
25
  import { defineRoles } from "./access/roles.js";
25
26
  import { pickView } from "./access/pick-view.js";
26
27
  import { restrictModel } from "./access/restrict.js";
27
- export { accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
28
+ export { 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
@@ -1,4 +1,4 @@
1
- import { CardPhotosModel, HelpWhen, LayoutItemModel, RelatedModel, SectionPresentation, ShowWhen } from "./model.js";
1
+ import { CardPhotosModel, HelpWhen, LayoutItemModel, RecordWhen, RelatedModel, SectionPresentation, ShowWhen } from "./model.js";
2
2
  import { Ref } from "./refs.js";
3
3
  import { ComponentRef, ViewProps } from "./view-displays.js";
4
4
  //#region src/layout.d.ts
@@ -31,7 +31,7 @@ export declare const layout: {
31
31
  help?: string;
32
32
  helpWhen?: HelpWhen;
33
33
  presentation?: SectionPresentation;
34
- when?: ShowWhen;
34
+ when?: RecordWhen;
35
35
  }) => LayoutItemModel;
36
36
  /**
37
37
  * The record's photos as tiles in the main column, one large and the next four smaller, the last saying how many more
@@ -48,7 +48,16 @@ export declare const layout: {
48
48
  }) => LayoutItemModel;
49
49
  /** A read-only summary card of fields beside the main column. */
50
50
  sidebar: (fields: Ref[], o?: {
51
- when?: ShowWhen;
51
+ when?: RecordWhen;
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;
52
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;
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';
@@ -288,6 +290,8 @@ export type HelpWhen = 'always' | 'editing';
288
290
  * [Form and read view](website/src/content/docs/reference/ui-config.mdx#form-and-read-view).
289
291
  */
290
292
  export type ShowWhen = 'always' | 'editing' | 'reading';
293
+ /** `ShowWhen`, or an AIP-160 condition over the saved record, as `<Show when>` takes: the item shows while it holds. See "Form and read view" in the UI reference. */
294
+ export type RecordWhen = ShowWhen | (string & {});
291
295
  /** How a field's badges are drawn: `arrow` points them right, as on a rating scale. */
292
296
  export type BadgeShape = 'arrow';
293
297
  /** A field type by the name a plugin registers it under, and the plain data it is given. */
@@ -640,8 +644,10 @@ export type RelatedModel = {
640
644
  display?: string;
641
645
  /** The text when nothing matches. */
642
646
  empty?: string;
643
- /** Whether it shows on the form, the read view or both. */
644
- when?: ShowWhen;
647
+ /** Leave the section off the page while it has no records. */
648
+ hideEmpty?: boolean;
649
+ /** Whether it shows on the form, the read view or both, or a condition on the saved record that it shows while it holds. */
650
+ when?: RecordWhen;
645
651
  };
646
652
  /**
647
653
  * A project component drawn on the record page where it is listed, in the main column or with `sidebar` in the
@@ -710,8 +716,8 @@ export type CommentsSectionModel = {
710
716
  };
711
717
  /** How a section shows its fields on the read view: `facts` as a row of key facts; label/value rows when omitted. */
712
718
  export type SectionPresentation = 'facts';
713
- /** 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). */
714
- 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 = {
715
721
  /** A titled group of fields in the main column, editable where the user may write. */
716
722
  kind: 'section';
717
723
  /** The section's title. */
@@ -727,16 +733,33 @@ export type LayoutItemModel = {
727
733
  * label/value rows; the form edits them as any section's.
728
734
  */
729
735
  presentation?: SectionPresentation;
730
- /** Whether it shows on the form, the read view or both. */
731
- when?: ShowWhen;
732
- } | {
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. */
737
+ when?: RecordWhen;
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 | {
733
741
  /** A read-only summary card beside the main column. */
734
742
  kind: 'sidebar';
735
743
  /** The fields it shows, in order. */
736
744
  fields: string[];
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. */
746
+ when?: RecordWhen;
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;
737
758
  /** Whether it shows on the form, the read view or both. */
738
- when?: ShowWhen;
739
- } | RelatedModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel | CommentsSectionModel;
759
+ when?: RecordWhen;
760
+ };
761
+ /** An item a group may hold. */
762
+ export type GroupItemModel = SectionModel | RelatedModel;
740
763
  /**
741
764
  * A record's photos as tiles in the main column under `title`, one large and the next few smaller, opening a lightbox
742
765
  * that steps through them all: first the record's own file field `image`, then the `image` of the records of
@@ -0,0 +1,4 @@
1
+ //#region src/record-when.d.ts
2
+ /** Whether `when` is a condition on the saved record rather than one of the `ShowWhen` modes. */
3
+ export declare const isRecordCondition: (when: string | undefined) => when is string;
4
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/record-when.ts
2
+ const showModes = [
3
+ "always",
4
+ "editing",
5
+ "reading"
6
+ ];
7
+ /** Whether `when` is a condition on the saved record rather than one of the `ShowWhen` modes. */
8
+ const isRecordCondition = (when) => when !== void 0 && !showModes.includes(when);
9
+ //#endregion
10
+ export { isRecordCondition };
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.10",
3
+ "version": "0.21.12",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {