@protobase/schema 0.19.15 → 0.19.16

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
@@ -19,7 +19,7 @@ import { UserMenuSource, userMenu } from "./user-menu.js";
19
19
  import { BottomBarSource, bottomBar, maxBottomBarItems } from "./bottom-bar.js";
20
20
  import { BottomBarNames, bottomBarErrors } from "./bottom-bar-errors.js";
21
21
  import { layout } from "./layout.js";
22
- import { KeyType, keyTypes } from "./keys.js";
22
+ import { KeyType, fitsKeyType, keyTypes } from "./keys.js";
23
23
  import { addressWords, appPath, readAppPath } from "./app-address.js";
24
24
  import { SignInAddress, SignInStep, readSignInAddress, signInParams, signInPath, signInRoute, signInSteps } from "./sign-in-address.js";
25
25
  import { FilterLimits, FilterParse, parseFilter } from "./filter/parse.js";
@@ -38,4 +38,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
38
38
  import { restrictModel } from "./access/restrict.js";
39
39
  import "./access/index.js";
40
40
  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 };
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, 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,6 @@
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 { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
4
4
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
5
5
  import { bottomBarFor } from "./bottom-bar.js";
6
6
  import { decodeKey, encodeKey } from "./keys.js";
@@ -15,4 +15,4 @@ import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlacehol
15
15
  import { AddressCheck, addressErrors } from "./address-errors.js";
16
16
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
17
17
  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 };
18
+ export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, 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 };
package/dist/internal.js CHANGED
@@ -5,7 +5,7 @@ import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.j
5
5
  import { actionInputErrors, shownInputs } from "./action-inputs.js";
6
6
  import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
7
7
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
8
- import { cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
8
+ import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
9
9
  import { bottomBarFor } from "./bottom-bar.js";
10
10
  import { decodeKey, encodeKey } from "./keys.js";
11
11
  import { webhookResources } from "./webhooks/webhook-resources.js";
@@ -15,4 +15,4 @@ import { redactSecrets } from "./redact-secrets.js";
15
15
  import { addressErrors } from "./address-errors.js";
16
16
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
17
17
  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 };
18
+ export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, 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 };
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
@@ -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: {
@@ -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
@@ -66,14 +66,17 @@ export declare class ViewBuilder<R extends {
66
66
  /** Hints per field, such as `{ total: r.total.prefix('€').decimals(2) }`; calls add up. */
67
67
  fields(build: (r: FieldViews<R>) => Record<string, FieldView>): ViewBuilder<R>;
68
68
  /**
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.
69
+ * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, the
70
+ * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.custom`), one of which
71
+ * may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
72
+ * editable field is not among the columns. See
73
+ * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
72
74
  */
73
75
  list(build: (r: FieldRefs<FieldsOf<R>>, d: typeof displays, c: typeof columns) => {
74
76
  columns: Array<Ref | ListColumn>;
75
77
  sort?: Array<[Ref, 'asc' | 'desc']>;
76
78
  search?: Ref[];
79
+ editable?: Ref[];
77
80
  displays?: ListDisplayModel[];
78
81
  default?: string;
79
82
  }): ViewBuilder<R>;
package/dist/view.js CHANGED
@@ -3,7 +3,7 @@ import { isMenuHref } from "./menu-href.js";
3
3
  import { navRecentErrors } from "./nav-recent.js";
4
4
  import { actions } from "./view-actions.js";
5
5
  import { columns, isListColumn } from "./view-columns.js";
6
- import { displayErrors, displayFields, displays } from "./view-displays.js";
6
+ import { boardErrors, displayErrors, displayFields, displays } from "./view-displays.js";
7
7
  import { FieldView } from "./view-field.js";
8
8
  import { widgets } from "./view-widgets.js";
9
9
  //#region src/view.ts
@@ -73,21 +73,26 @@ var ViewBuilder = class ViewBuilder {
73
73
  } });
74
74
  }
75
75
  /**
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.
76
+ * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, the
77
+ * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.custom`), one of which
78
+ * may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
79
+ * editable field is not among the columns. See
80
+ * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
79
81
  */
80
82
  list(build) {
81
- const { columns: columns$1, sort, search, displays: shown = [], default: opens } = build(this.refs(), displays, columns);
83
+ const { columns: columns$1, sort, search, editable, displays: shown = [], default: opens } = build(this.refs(), displays, columns);
82
84
  const related = columns$1.filter(isListColumn).map(({ kind: _, ...column }) => column);
83
85
  const names = columns$1.map((column) => column.name);
84
86
  const repeated = names.find((name, index) => names.indexOf(name) !== index);
85
87
  if (repeated !== void 0) throw new Error(`View "${this.state.resource}": the list has two columns named "${repeated}"`);
88
+ const stray = editable?.find((ref) => !names.includes(ref.name));
89
+ if (stray) throw new Error(`View "${this.state.resource}": editable field "${stray.name}" is not among the list's columns`);
86
90
  const list = {
87
91
  columns: names,
88
92
  ...related.length > 0 && { relatedColumns: related },
89
93
  ...sort && { sort: sort.map(([ref, dir]) => [ref.name, dir]) },
90
94
  ...search && { search: search.map((ref) => ref.name) },
95
+ ...editable && editable.length > 0 && { editable: editable.map((ref) => ref.name) },
91
96
  ...shown.length > 0 && { displays: shown },
92
97
  ...opens !== void 0 && { default: opens }
93
98
  };
@@ -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.19.16",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {