@protobase/schema 0.19.15 → 0.20.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.
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, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, GalleryModel, HelpWhen, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, 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, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, GalleryModel, GroupChartModel, HelpWhen, 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";
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 { ChartInput, GroupChartInput, PeriodChartInput } from "./view-chart.js";
12
13
  import { ListColumn } from "./view-columns.js";
13
14
  import { ComponentRef, ViewProps } from "./view-displays.js";
14
15
  import { FieldView } from "./view-field.js";
@@ -19,7 +20,7 @@ import { UserMenuSource, userMenu } from "./user-menu.js";
19
20
  import { BottomBarSource, bottomBar, maxBottomBarItems } from "./bottom-bar.js";
20
21
  import { BottomBarNames, bottomBarErrors } from "./bottom-bar-errors.js";
21
22
  import { layout } from "./layout.js";
22
- import { KeyType, keyTypes } from "./keys.js";
23
+ import { KeyType, fitsKeyType, keyTypes } from "./keys.js";
23
24
  import { addressWords, appPath, readAppPath } from "./app-address.js";
24
25
  import { SignInAddress, SignInStep, readSignInAddress, signInParams, signInPath, signInRoute, signInSteps } from "./sign-in-address.js";
25
26
  import { FilterLimits, FilterParse, parseFilter } from "./filter/parse.js";
@@ -38,4 +39,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
38
39
  import { restrictModel } from "./access/restrict.js";
39
40
  import "./access/index.js";
40
41
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
41
- 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 Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, 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 GalleryModel, type Grant, type HeldCapability, type HelpWhen, 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 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, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
42
+ 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 Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, 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 GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, 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, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
package/dist/index.js CHANGED
@@ -16,11 +16,11 @@ import { userMenu } from "./user-menu.js";
16
16
  import { bottomBar, maxBottomBarItems } from "./bottom-bar.js";
17
17
  import { bottomBarErrors } from "./bottom-bar-errors.js";
18
18
  import { layout } from "./layout.js";
19
- import { keyTypes } from "./keys.js";
19
+ import { fitsKeyType, keyTypes } from "./keys.js";
20
20
  import { addressWords, appPath, readAppPath } from "./app-address.js";
21
21
  import { readSignInAddress, signInParams, signInPath, signInRoute, signInSteps } from "./sign-in-address.js";
22
22
  import { rule } from "./access/rule.js";
23
23
  import { defineRoles } from "./access/roles.js";
24
24
  import { pickView } from "./access/pick-view.js";
25
25
  import { restrictModel } from "./access/restrict.js";
26
- export { accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
26
+ export { accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
@@ -1,6 +1,7 @@
1
1
  import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
2
2
  import { actionInputErrors, shownInputs } from "./action-inputs.js";
3
- import { cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
3
+ import { chartFields, isGroupChart } from "./view-chart.js";
4
+ import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
4
5
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
5
6
  import { bottomBarFor } from "./bottom-bar.js";
6
7
  import { decodeKey, encodeKey } from "./keys.js";
@@ -15,4 +16,4 @@ import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlacehol
15
16
  import { AddressCheck, addressErrors } from "./address-errors.js";
16
17
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
17
18
  import { readEventStream } from "./assistant/event-stream.js";
18
- export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, bottomBarFor, cardPhotoLimit, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
19
+ export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, 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, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/internal.js CHANGED
@@ -4,8 +4,9 @@ import { checkEventFilter } from "./events/event-models.js";
4
4
  import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
5
5
  import { actionInputErrors, shownInputs } from "./action-inputs.js";
6
6
  import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
7
+ import { chartFields, isGroupChart } from "./view-chart.js";
7
8
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
8
- import { cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
9
+ import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
9
10
  import { bottomBarFor } from "./bottom-bar.js";
10
11
  import { decodeKey, encodeKey } from "./keys.js";
11
12
  import { webhookResources } from "./webhooks/webhook-resources.js";
@@ -15,4 +16,4 @@ import { redactSecrets } from "./redact-secrets.js";
15
16
  import { addressErrors } from "./address-errors.js";
16
17
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
17
18
  import { readEventStream } from "./assistant/event-stream.js";
18
- export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, bottomBarFor, cardPhotoLimit, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
19
+ export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, 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, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/keys.d.ts CHANGED
@@ -2,6 +2,8 @@ import { FieldType, KeyValue, ResourceModel } from "./model.js";
2
2
  //#region src/keys.d.ts
3
3
  /** The field types a primary key part may have: `integer`, `bigint`, `uuid` or `text`. */
4
4
  export type KeyType = Extract<FieldType, 'integer' | 'bigint' | 'uuid' | 'text'>;
5
+ /** Whether a column of `type` can hold `value`, such as a user id an owner field stores. */
6
+ export declare const fitsKeyType: (value: string, type: KeyType) => boolean;
5
7
  export declare const encodeKey: (key: KeyValue | readonly KeyValue[]) => string;
6
8
  export declare const decodeKey: (encoded: string, types: readonly KeyType[]) => string | number | (string | number)[];
7
9
  /**
package/dist/keys.js CHANGED
@@ -13,6 +13,15 @@ const decodePart = (part, type) => {
13
13
  if (!Number.isSafeInteger(value)) throw new Error("Invalid integer key part");
14
14
  return value;
15
15
  };
16
+ /** Whether a column of `type` can hold `value`, such as a user id an owner field stores. */
17
+ const fitsKeyType = (value, type) => {
18
+ if (type === "text") return true;
19
+ if (type === "uuid") return uuidPattern.test(value);
20
+ if (!integerPattern.test(value)) return false;
21
+ if (type === "bigint") return true;
22
+ const number = Number(value);
23
+ return Number.isSafeInteger(number) && Math.abs(number) <= 2 ** 31 - 1;
24
+ };
16
25
  const decodeComponent = (part) => {
17
26
  try {
18
27
  return decodeURIComponent(part);
@@ -54,4 +63,4 @@ const keyTypes = (model, resolve, seen = []) => model.primaryKey.flatMap((name)
54
63
  return types;
55
64
  });
56
65
  //#endregion
57
- export { decodeKey, encodeKey, keyTypes };
66
+ export { decodeKey, encodeKey, fitsKeyType, keyTypes };
package/dist/model.d.ts CHANGED
@@ -304,6 +304,8 @@ export type ListDisplayModel = {
304
304
  footer?: string[];
305
305
  /** Boolean fields shown as small badges of their labels when true. */
306
306
  flags?: string[];
307
+ /** Related columns of the list (`c.related`) shown as lines of their dots at the card's bottom, by their names. */
308
+ dots?: string[];
307
309
  /** The component that draws each card's content instead of the built-in card. */
308
310
  card?: string;
309
311
  /** Where the card's further photos come from. */
@@ -335,6 +337,25 @@ export type ListDisplayModel = {
335
337
  defaultRadius?: number;
336
338
  /** The field holding the GeoJSON geometry around an approximate position: a polygon, or a line such as a street. */
337
339
  area?: string;
340
+ } | {
341
+ /** A column per value of an enum field, a card per record in its value's column, moved by dragging. */
342
+ kind: 'board';
343
+ /** What the address names (`?display=board`). */
344
+ name: string;
345
+ /** The display's name in the switch. */
346
+ label?: string;
347
+ /** The display's icon in the switch. */
348
+ icon?: string;
349
+ /** The filterable enum field whose values are the columns, and which a card dragged to another column sets. */
350
+ by: string;
351
+ /** The card's title; the view's title when omitted. */
352
+ title?: string;
353
+ /** The line under the title. */
354
+ subtitle?: string;
355
+ /** A field drawn as a badge with its value labels. */
356
+ badge?: string;
357
+ /** A few small fields, such as dates, each shown when it has a value. */
358
+ meta?: string[];
338
359
  } | {
339
360
  /** The rows drawn by a project component. */
340
361
  kind: 'custom';
@@ -465,6 +486,11 @@ export type ListModel = {
465
486
  default?: string;
466
487
  /** The columns of related records among `columns`, each named there by its `name`. */
467
488
  relatedColumns?: RelatedColumnModel[];
489
+ /**
490
+ * The fields among `columns` edited in their cell, by a user who may change the record and the field; see
491
+ * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
492
+ */
493
+ editable?: string[];
468
494
  };
469
495
  /**
470
496
  * A list column of the records of another resource that point at each row, directly or through a join resource, as a
@@ -661,8 +687,8 @@ export type GalleryModel = {
661
687
  export type ChartRange = '7d' | '30d' | '90d' | '1y' | 'all';
662
688
  /** How long each bar of a list's chart is. */
663
689
  export type ChartGranularity = 'hour' | 'day' | 'week' | 'month';
664
- /** A chart of records over time above a list, following its filters (`.chart(...)`). */
665
- export type ChartModel = {
690
+ /** A chart of records over time above a list, following its filters (`.chart(...)` with `field`). */
691
+ export type PeriodChartModel = {
666
692
  /** The date or timestamp field the records are counted by. */
667
693
  field: string;
668
694
  /** How far back it reaches. */
@@ -670,6 +696,21 @@ export type ChartModel = {
670
696
  /** How long each bar is. */
671
697
  granularity: ChartGranularity;
672
698
  };
699
+ /** A chart of records per value of a field above a list, following its filters (`.chart(...)` with `groupBy`). */
700
+ export type GroupChartModel = {
701
+ /** The field whose values make the groups, such as a status or a relation; not a date. */
702
+ groupBy: string;
703
+ /** Bars, the largest first, or a donut. */
704
+ type: 'bar' | 'donut';
705
+ /** A numeric field each group adds up, instead of counting its records. */
706
+ sum?: string;
707
+ /** A numeric field each group averages, instead of counting its records. */
708
+ average?: string;
709
+ /** How many values it shows, the largest. */
710
+ limit: number;
711
+ };
712
+ /** The chart above a list: records over time, or records per value of a field. */
713
+ export type ChartModel = PeriodChartModel | GroupChartModel;
673
714
  /**
674
715
  * How the record page opens to a user who may change the record: `edit`, the default, as its form, ready to change;
675
716
  * `read` as a read view of label/value tables, with an Edit button to the form; `locked` as the same form, locked until
@@ -17,7 +17,10 @@ export declare const actions: {
17
17
  bulk?: boolean;
18
18
  inputs?: (i: typeof inputs) => ActionInputModel[];
19
19
  }) => ActionModel;
20
- /** Sets fields of the record, as one update with its ETag, for example `{ status: 'paid' }`. */
20
+ /**
21
+ * Sets fields of the record, as one update with its ETag, for example `{ status: 'paid' }`. It is not offered on a
22
+ * record that holds these values already.
23
+ */
21
24
  update: (name: string, o: ActionOptions & {
22
25
  set: Record<string, unknown>;
23
26
  }) => ActionModel;
@@ -24,7 +24,10 @@ const actions = {
24
24
  inputs: built
25
25
  };
26
26
  },
27
- /** Sets fields of the record, as one update with its ETag, for example `{ status: 'paid' }`. */
27
+ /**
28
+ * Sets fields of the record, as one update with its ETag, for example `{ status: 'paid' }`. It is not offered on a
29
+ * record that holds these values already.
30
+ */
28
31
  update: (name, o) => ({
29
32
  ...named(name, o),
30
33
  run: {
@@ -0,0 +1,32 @@
1
+ import { ChartGranularity, ChartModel, ChartRange, GroupChartModel } from "./model.js";
2
+ import { Ref } from "./refs.js";
3
+ //#region src/view-chart.d.ts
4
+ /** Records per `granularity` over a date or timestamp `field`. */
5
+ export type PeriodChartInput = {
6
+ /** The date or timestamp field the records are counted by. */
7
+ field: Ref;
8
+ /** How far back it reaches. */
9
+ range: ChartRange;
10
+ /** How long each bar is. */
11
+ granularity: ChartGranularity;
12
+ };
13
+ /** Records per value of `groupBy`, counted, or with `sum` or `average` that of a numeric field. */
14
+ export type GroupChartInput = {
15
+ /** The field whose values make the groups, such as a status or a relation; not a date. */
16
+ groupBy: Ref;
17
+ /** Bars, the default, or a donut. */
18
+ type?: GroupChartModel['type'];
19
+ /** How many values it shows, the largest; default 10. */
20
+ limit?: number;
21
+ /** A numeric field each group adds up, instead of counting its records. */
22
+ sum?: Ref;
23
+ /** A numeric field each group averages, instead of counting its records. */
24
+ average?: Ref;
25
+ };
26
+ /** What `.chart(...)` takes: a chart over time or per value. */
27
+ export type ChartInput = PeriodChartInput | GroupChartInput;
28
+ /** Whether a list's chart counts records per value of a field rather than over time. */
29
+ export declare const isGroupChart: (chart: ChartModel) => chart is GroupChartModel;
30
+ /** The fields a list's chart reads: its date field, or the field it groups by and the one it adds up. */
31
+ export declare const chartFields: (chart: ChartModel) => string[];
32
+ //#endregion
@@ -0,0 +1,29 @@
1
+ //#region src/view-chart.ts
2
+ const defaultLimit = 10;
3
+ const chartModel = (input) => {
4
+ if (!("groupBy" in input)) return {
5
+ field: input.field.name,
6
+ range: input.range,
7
+ granularity: input.granularity
8
+ };
9
+ if (input.sum && input.average) throw new Error("A chart takes sum or average, not both");
10
+ const limit = input.limit ?? defaultLimit;
11
+ if (!Number.isInteger(limit) || limit < 1) throw new Error(`A chart's limit must be a positive whole number, not ${limit}`);
12
+ return {
13
+ groupBy: input.groupBy.name,
14
+ type: input.type ?? "bar",
15
+ ...input.sum && { sum: input.sum.name },
16
+ ...input.average && { average: input.average.name },
17
+ limit
18
+ };
19
+ };
20
+ /** Whether a list's chart counts records per value of a field rather than over time. */
21
+ const isGroupChart = (chart) => "groupBy" in chart;
22
+ /** The fields a list's chart reads: its date field, or the field it groups by and the one it adds up. */
23
+ const chartFields = (chart) => isGroupChart(chart) ? [
24
+ chart.groupBy,
25
+ ...chart.sum ? [chart.sum] : [],
26
+ ...chart.average ? [chart.average] : []
27
+ ] : [chart.field];
28
+ //#endregion
29
+ export { chartFields, chartModel, isGroupChart };
@@ -1,5 +1,6 @@
1
- import { CardFavoriteModel, CardPhotosModel, ListDisplayModel } from "./model.js";
1
+ import { CardFavoriteModel, CardPhotosModel, ListDisplayModel, ResourceModel } from "./model.js";
2
2
  import { Ref } from "./refs.js";
3
+ import { ListColumn } from "./view-columns.js";
3
4
  //#region src/view-displays.d.ts
4
5
  /** A custom component as `component(name)` from `@protobase/layout` declares it. */
5
6
  export type ComponentRef<P> = ((props: P) => unknown) & {
@@ -26,6 +27,8 @@ export declare const displays: {
26
27
  * view's title) over `subtitle`, `badge` drawn as a badge with its value labels, `amount` the first of its fields with a
27
28
  * value in bold, such as a purchase price or a rent, and a few small `meta` fields such as dates. `footer` is a line at
28
29
  * the card's bottom of its fields with a value, joined by bullets. `flags` are boolean fields shown as small badges of their labels when true, such as a garden or parking.
30
+ * `dots` are related columns of the list (`c.related`), each a line of its dots under the rest of the card, with the
31
+ * colours, order and hover cards they have in the table; a column with no records for the row shows no line.
29
32
  * With `card`, a component of the project or a plugin draws each card's content instead, from the row of those
30
33
  * fields (`{ row }`); without it, or when it throws, the card is the built-in one. With `photos`, the built-in card's
31
34
  * picture gets arrows through the `image` of the records of another resource that point at the card's record, after
@@ -43,6 +46,7 @@ export declare const displays: {
43
46
  meta?: Ref[];
44
47
  footer?: Ref[];
45
48
  flags?: Ref[];
49
+ dots?: ListColumn[];
46
50
  }, o?: Naming & {
47
51
  card?: ComponentRef<never>;
48
52
  photos?: CardPhotosModel;
@@ -70,6 +74,24 @@ export declare const displays: {
70
74
  cards?: string;
71
75
  defaultRadius?: number;
72
76
  }) => ListDisplayModel;
77
+ /**
78
+ * A column per value of the enum field `by`, in the order the field lists them, each headed by its value label and how
79
+ * many of the list's records have it; with a column for records without a value when the field may be empty. Each
80
+ * column pages through the list API with the list's filter and search, its value added, in the list's order. A card
81
+ * shows `title` (default: the view's title) over `subtitle`, with `badge` and a few small `meta` fields, as the built-in
82
+ * card of `d.cards` does, and opens the record. Dragging a card to another column, or picking it up with Space and
83
+ * moving it with the arrow keys, saves the record with `by` set to that column's value, as one update with its ETag,
84
+ * so the resource's access rules and `validate` decide, and a refusal puts the card back and says why. `by` must be
85
+ * a filterable enum field. Named `board` unless `name` says otherwise. See
86
+ * [Boards](website/src/content/docs/reference/ui-config.mdx#boards).
87
+ */
88
+ board: (fields: {
89
+ by: Ref;
90
+ title?: Ref;
91
+ subtitle?: Ref;
92
+ badge?: Ref;
93
+ meta?: Ref[];
94
+ }, o?: Naming) => ListDisplayModel;
73
95
  /**
74
96
  * The list drawn by a component of the project, registered in `protobase.ui.tsx` under its name. It gets `props` and
75
97
  * reads the rows with `useListDisplay()`; `fields` are the fields it shows, fetched with relation names.
@@ -81,6 +103,10 @@ export declare const displays: {
81
103
  props?: ViewProps<P>;
82
104
  }) => ListDisplayModel;
83
105
  };
84
- /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, and the field references in a custom display's props. */
106
+ /** What makes a board unable to put the records in columns: a `by` field that is no filterable enum. */
107
+ export declare const boardErrors: (display: Extract<ListDisplayModel, {
108
+ kind: "board";
109
+ }>, model: ResourceModel) => string[];
110
+ /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, and the field references in a custom display's props. */
85
111
  export declare const displayFields: (display: ListDisplayModel) => string[];
86
112
  //#endregion
@@ -19,6 +19,8 @@ const displays = {
19
19
  * view's title) over `subtitle`, `badge` drawn as a badge with its value labels, `amount` the first of its fields with a
20
20
  * value in bold, such as a purchase price or a rent, and a few small `meta` fields such as dates. `footer` is a line at
21
21
  * the card's bottom of its fields with a value, joined by bullets. `flags` are boolean fields shown as small badges of their labels when true, such as a garden or parking.
22
+ * `dots` are related columns of the list (`c.related`), each a line of its dots under the rest of the card, with the
23
+ * colours, order and hover cards they have in the table; a column with no records for the row shows no line.
22
24
  * With `card`, a component of the project or a plugin draws each card's content instead, from the row of those
23
25
  * fields (`{ row }`); without it, or when it throws, the card is the built-in one. With `photos`, the built-in card's
24
26
  * picture gets arrows through the `image` of the records of another resource that point at the card's record, after
@@ -39,6 +41,7 @@ const displays = {
39
41
  ...fields.meta && { meta: fields.meta.map((ref) => ref.name) },
40
42
  ...fields.footer && { footer: fields.footer.map((ref) => ref.name) },
41
43
  ...fields.flags && { flags: fields.flags.map((ref) => ref.name) },
44
+ ...fields.dots && { dots: fields.dots.map((column) => column.name) },
42
45
  ...o.card && { card: customComponent(o.card, "d.cards card") },
43
46
  ...o.photos && { photos: { ...o.photos } },
44
47
  ...o.favorite && { favorite: { ...o.favorite } }
@@ -68,6 +71,27 @@ const displays = {
68
71
  ...fields.area && { area: fields.area.name }
69
72
  }),
70
73
  /**
74
+ * A column per value of the enum field `by`, in the order the field lists them, each headed by its value label and how
75
+ * many of the list's records have it; with a column for records without a value when the field may be empty. Each
76
+ * column pages through the list API with the list's filter and search, its value added, in the list's order. A card
77
+ * shows `title` (default: the view's title) over `subtitle`, with `badge` and a few small `meta` fields, as the built-in
78
+ * card of `d.cards` does, and opens the record. Dragging a card to another column, or picking it up with Space and
79
+ * moving it with the arrow keys, saves the record with `by` set to that column's value, as one update with its ETag,
80
+ * so the resource's access rules and `validate` decide, and a refusal puts the card back and says why. `by` must be
81
+ * a filterable enum field. Named `board` unless `name` says otherwise. See
82
+ * [Boards](website/src/content/docs/reference/ui-config.mdx#boards).
83
+ */
84
+ board: (fields, o = {}) => ({
85
+ kind: "board",
86
+ name: o.name ?? "board",
87
+ ...naming(o),
88
+ by: fields.by.name,
89
+ ...fields.title && { title: fields.title.name },
90
+ ...fields.subtitle && { subtitle: fields.subtitle.name },
91
+ ...fields.badge && { badge: fields.badge.name },
92
+ ...fields.meta && { meta: fields.meta.map((ref) => ref.name) }
93
+ }),
94
+ /**
71
95
  * The list drawn by a component of the project, registered in `protobase.ui.tsx` under its name. It gets `props` and
72
96
  * reads the rows with `useListDisplay()`; `fields` are the fields it shows, fetched with relation names.
73
97
  */
@@ -90,6 +114,7 @@ const displayErrors = (list) => {
90
114
  ...names.filter((name) => name === tableDisplay).map(() => `"${tableDisplay}" is the list's own display; give the display another name`),
91
115
  ...names.filter((name, index) => names.indexOf(name) !== index).map((name) => `display "${name}" is defined twice`),
92
116
  ...(list.displays ?? []).flatMap((display) => display.kind === "map" && display.cards !== void 0 && !list.displays.some((other) => other.kind === "cards" && other.name === display.cards) ? [`display "${display.name}" names cards "${display.cards}", which is not a cards display of the list`] : []),
117
+ ...(list.displays ?? []).flatMap((display) => display.kind === "cards" ? (display.dots ?? []).filter((name) => !list.relatedColumns?.some((column) => column.name === name)).map((name) => `display "${display.name}" shows dots of "${name}", which is not a related column of the list`) : []),
93
118
  ...(list.displays ?? []).flatMap((display) => display.kind === "map" ? approximateErrors(display).map((error) => `display "${display.name}": ${error}`) : []),
94
119
  ...list.default === void 0 || list.default === "table" || names.includes(list.default) ? [] : [`default "${list.default}" is not one of the displays (${[tableDisplay, ...names].join(", ")})`]
95
120
  ];
@@ -99,7 +124,14 @@ const approximateErrors = (display) => {
99
124
  if (approximate !== void 0 && exact !== void 0) return ["name approximate or exact, not both"];
100
125
  return [...(radius !== void 0 || defaultRadius !== void 0 || area !== void 0) && approximate === void 0 && exact === void 0 ? ["an area around approximate positions needs the approximate or exact field"] : [], ...defaultRadius === void 0 || Number.isFinite(defaultRadius) && defaultRadius > 0 ? [] : [`defaultRadius ${defaultRadius} is not a number of metres above 0`]];
101
126
  };
102
- /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, and the field references in a custom display's props. */
127
+ /** What makes a board unable to put the records in columns: a `by` field that is no filterable enum. */
128
+ const boardErrors = (display, model) => {
129
+ const field = Object.hasOwn(model.fields, display.by) ? model.fields[display.by] : void 0;
130
+ if (!field) return [];
131
+ if (field.type !== "enum") return [`"${display.by}" on ${model.name} is ${field.type}, not an enum, whose values are the board's columns`];
132
+ return field.filterable ? [] : [`"${display.by}" on ${model.name} is not filterable, and each column of the board filters by its value`];
133
+ };
134
+ /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, and the field references in a custom display's props. */
103
135
  const displayFields = (display) => {
104
136
  if (display.kind === "map") return [
105
137
  display.latitude,
@@ -110,6 +142,13 @@ const displayFields = (display) => {
110
142
  display.area
111
143
  ].filter((name) => name !== void 0);
112
144
  if (display.kind === "custom") return [...display.fields, ...fieldRefsIn(display.props)];
145
+ if (display.kind === "board") return [
146
+ display.by,
147
+ display.title,
148
+ display.subtitle,
149
+ display.badge,
150
+ ...display.meta ?? []
151
+ ].filter((name) => name !== void 0);
113
152
  return [
114
153
  display.image,
115
154
  display.title,
@@ -122,4 +161,4 @@ const displayFields = (display) => {
122
161
  ].filter((name) => name !== void 0);
123
162
  };
124
163
  //#endregion
125
- export { cardPhotoLimit, customComponent, displayErrors, displayFields, displays, tableDisplay };
164
+ export { boardErrors, cardPhotoLimit, customComponent, displayErrors, displayFields, displays, tableDisplay };
package/dist/view.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { ActionModel, ChartGranularity, ChartRange, CreateModel, ExportModel, FilterWidgetModel, HelpWhen, LayoutItemModel, ListDisplayModel, NavModel, NavRecentModel, RecordMode, ResourceModel, SaveFeedback, SaveSearchModel, SignInPromptModel, ViewModel } from "./model.js";
1
+ import { ActionModel, CreateModel, ExportModel, FilterWidgetModel, HelpWhen, LayoutItemModel, ListDisplayModel, NavModel, NavRecentModel, RecordMode, ResourceModel, SaveFeedback, SaveSearchModel, SignInPromptModel, ViewModel } from "./model.js";
2
2
  import { FieldRefs, Ref } from "./refs.js";
3
3
  import { actions } from "./view-actions.js";
4
+ import { ChartInput } from "./view-chart.js";
4
5
  import { ListColumn, columns } from "./view-columns.js";
5
6
  import { displays } from "./view-displays.js";
6
7
  import { FieldView } from "./view-field.js";
@@ -66,14 +67,17 @@ export declare class ViewBuilder<R extends {
66
67
  /** Hints per field, such as `{ total: r.total.prefix('€').decimals(2) }`; calls add up. */
67
68
  fields(build: (r: FieldViews<R>) => Record<string, FieldView>): ViewBuilder<R>;
68
69
  /**
69
- * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, and other
70
- * `displays` to switch to (`d.cards`, `d.custom`), one of which may be the `default` the list opens in. Throws when
71
- * display or column names collide or `default` names none.
70
+ * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, the
71
+ * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.custom`), one of which
72
+ * may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
73
+ * editable field is not among the columns. See
74
+ * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
72
75
  */
73
76
  list(build: (r: FieldRefs<FieldsOf<R>>, d: typeof displays, c: typeof columns) => {
74
77
  columns: Array<Ref | ListColumn>;
75
78
  sort?: Array<[Ref, 'asc' | 'desc']>;
76
79
  search?: Ref[];
80
+ editable?: Ref[];
77
81
  displays?: ListDisplayModel[];
78
82
  default?: string;
79
83
  }): ViewBuilder<R>;
@@ -87,12 +91,12 @@ export declare class ViewBuilder<R extends {
87
91
  * Without it the page shows one section with every field.
88
92
  */
89
93
  layout(build: (r: FieldRefs<FieldsOf<R>>) => LayoutItemModel[]): ViewBuilder<R>;
90
- /** A chart of records per `granularity` over a date or timestamp `field` above the list, following its filters. */
91
- chart(build: (r: FieldRefs<FieldsOf<R>>) => {
92
- field: Ref;
93
- range: ChartRange;
94
- granularity: ChartGranularity;
95
- }): ViewBuilder<R>;
94
+ /**
95
+ * A chart above the list, following its filters: records per `granularity` over a date or timestamp `field`, or
96
+ * records per value of `groupBy`, such as a status or a relation, as bars or a donut of the `limit` largest (default
97
+ * 10), counted or with `sum` or `average` that of a numeric field.
98
+ */
99
+ chart(build: (r: FieldRefs<FieldsOf<R>>) => ChartInput): ViewBuilder<R>;
96
100
  /**
97
101
  * How the record page opens to a user who may change the record: `edit`, the default, as its form; `read` as a read
98
102
  * view, its sections as tables of labels and values, with an Edit button to the form; `locked` as the form, locked
package/dist/view.js CHANGED
@@ -2,8 +2,9 @@ import { createRefs, ref } from "./refs.js";
2
2
  import { isMenuHref } from "./menu-href.js";
3
3
  import { navRecentErrors } from "./nav-recent.js";
4
4
  import { actions } from "./view-actions.js";
5
+ import { chartModel } from "./view-chart.js";
5
6
  import { columns, isListColumn } from "./view-columns.js";
6
- import { displayErrors, displayFields, displays } from "./view-displays.js";
7
+ import { boardErrors, displayErrors, displayFields, displays } from "./view-displays.js";
7
8
  import { FieldView } from "./view-field.js";
8
9
  import { widgets } from "./view-widgets.js";
9
10
  //#region src/view.ts
@@ -73,21 +74,26 @@ var ViewBuilder = class ViewBuilder {
73
74
  } });
74
75
  }
75
76
  /**
76
- * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, and other
77
- * `displays` to switch to (`d.cards`, `d.custom`), one of which may be the `default` the list opens in. Throws when
78
- * display or column names collide or `default` names none.
77
+ * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, the
78
+ * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.custom`), one of which
79
+ * may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
80
+ * editable field is not among the columns. See
81
+ * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
79
82
  */
80
83
  list(build) {
81
- const { columns: columns$1, sort, search, displays: shown = [], default: opens } = build(this.refs(), displays, columns);
84
+ const { columns: columns$1, sort, search, editable, displays: shown = [], default: opens } = build(this.refs(), displays, columns);
82
85
  const related = columns$1.filter(isListColumn).map(({ kind: _, ...column }) => column);
83
86
  const names = columns$1.map((column) => column.name);
84
87
  const repeated = names.find((name, index) => names.indexOf(name) !== index);
85
88
  if (repeated !== void 0) throw new Error(`View "${this.state.resource}": the list has two columns named "${repeated}"`);
89
+ const stray = editable?.find((ref) => !names.includes(ref.name));
90
+ if (stray) throw new Error(`View "${this.state.resource}": editable field "${stray.name}" is not among the list's columns`);
86
91
  const list = {
87
92
  columns: names,
88
93
  ...related.length > 0 && { relatedColumns: related },
89
94
  ...sort && { sort: sort.map(([ref, dir]) => [ref.name, dir]) },
90
95
  ...search && { search: search.map((ref) => ref.name) },
96
+ ...editable && editable.length > 0 && { editable: editable.map((ref) => ref.name) },
91
97
  ...shown.length > 0 && { displays: shown },
92
98
  ...opens !== void 0 && { default: opens }
93
99
  };
@@ -109,14 +115,13 @@ var ViewBuilder = class ViewBuilder {
109
115
  layout(build) {
110
116
  return this.next({ layout: build(this.refs()) });
111
117
  }
112
- /** A chart of records per `granularity` over a date or timestamp `field` above the list, following its filters. */
118
+ /**
119
+ * A chart above the list, following its filters: records per `granularity` over a date or timestamp `field`, or
120
+ * records per value of `groupBy`, such as a status or a relation, as bars or a donut of the `limit` largest (default
121
+ * 10), counted or with `sum` or `average` that of a numeric field.
122
+ */
113
123
  chart(build) {
114
- const { field, range, granularity } = build(this.refs());
115
- return this.next({ chart: {
116
- field: field.name,
117
- range,
118
- granularity
119
- } });
124
+ return this.next({ chart: chartModel(build(this.refs())) });
120
125
  }
121
126
  /**
122
127
  * How the record page opens to a user who may change the record: `edit`, the default, as its form; `read` as a read
@@ -237,6 +242,8 @@ var ViewBuilder = class ViewBuilder {
237
242
  if (display.kind === "cards" && display.image !== void 0 && resource.fields[display.image].type !== "file") throw new Error(`View "${this.state.resource}": display "${display.name}" shows "${display.image}" as its image, which is not a file field`);
238
243
  const flag = display.kind === "cards" ? display.flags?.find((name) => resource.fields[name].type !== "boolean") : void 0;
239
244
  if (flag !== void 0) throw new Error(`View "${this.state.resource}": display "${display.name}" shows "${flag}" as a flag, which is not a boolean field`);
245
+ const board = display.kind === "board" ? boardErrors(display, resource) : [];
246
+ if (board.length > 0) throw new Error(`View "${this.state.resource}": display "${display.name}": ${board.join("; ")}`);
240
247
  }
241
248
  const clash = resource ? list?.relatedColumns?.find((column) => Object.hasOwn(resource.fields, column.name)) : void 0;
242
249
  if (clash) throw new Error(`View "${this.state.resource}": related column "${clash.name}" has the name of a field of the resource`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.19.15",
3
+ "version": "0.20.0",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {