@protobase/schema 0.14.3 → 0.14.5
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/filter/evaluate.d.ts +9 -3
- package/dist/filter/evaluate.js +7 -5
- package/dist/index.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/model.d.ts +45 -2
- package/dist/view.d.ts +30 -4
- package/dist/view.js +47 -3
- package/package.json +1 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { CheckedFilter } from "../model.js";
|
|
1
|
+
import { CheckedFilter, ResourceModel } from "../model.js";
|
|
2
2
|
//#region src/filter/evaluate.d.ts
|
|
3
3
|
/** How `evaluateFilter` evaluates. */
|
|
4
4
|
export type EvaluateOptions = {
|
|
5
5
|
/** Clock for `now()`; defaults to the current time. */
|
|
6
6
|
now?: () => Date;
|
|
7
|
+
/**
|
|
8
|
+
* The resource the record is of: `search("text")` then looks in its search fields as the API's search does, instead of
|
|
9
|
+
* throwing `Not implemented`.
|
|
10
|
+
*/
|
|
11
|
+
search?: ResourceModel;
|
|
7
12
|
};
|
|
8
13
|
/**
|
|
9
|
-
* Evaluates a checked filter against a plain record keyed by field name, for rules such as
|
|
10
|
-
*
|
|
14
|
+
* Evaluates a checked filter against a plain record keyed by field name, for rules such as access filters on create, or
|
|
15
|
+
* whether a new record fits a saved search. Throws `Not implemented` for `similar`, and for `search` without the
|
|
16
|
+
* `search` option.
|
|
11
17
|
*/
|
|
12
18
|
export declare const evaluateFilter: (checked: CheckedFilter, record: unknown, options?: EvaluateOptions) => boolean;
|
|
13
19
|
//#endregion
|
package/dist/filter/evaluate.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { functions } from "./profile.js";
|
|
2
2
|
import { lift } from "./lift.js";
|
|
3
|
+
import { matchesSearch } from "../search-match.js";
|
|
3
4
|
import { evaluateFilter } from "aip-parsers/filter";
|
|
4
5
|
//#region src/filter/evaluate.ts
|
|
5
6
|
const needsDatabase = (name) => () => {
|
|
6
7
|
throw new Error(`Not implemented: ${name}() needs the database (the resource's search fields, trigram matching), so it cannot be evaluated against a record`);
|
|
7
8
|
};
|
|
8
|
-
const implementations = (now) => ({
|
|
9
|
+
const implementations = (now, record, model) => ({
|
|
9
10
|
...functions,
|
|
10
11
|
search: {
|
|
11
12
|
...functions.search,
|
|
12
|
-
evaluate: needsDatabase("search")
|
|
13
|
+
evaluate: model ? (text) => matchesSearch(model, record, text) : needsDatabase("search")
|
|
13
14
|
},
|
|
14
15
|
similar: {
|
|
15
16
|
...functions.similar,
|
|
@@ -25,9 +26,10 @@ const implementations = (now) => ({
|
|
|
25
26
|
}
|
|
26
27
|
});
|
|
27
28
|
/**
|
|
28
|
-
* Evaluates a checked filter against a plain record keyed by field name, for rules such as
|
|
29
|
-
*
|
|
29
|
+
* Evaluates a checked filter against a plain record keyed by field name, for rules such as access filters on create, or
|
|
30
|
+
* whether a new record fits a saved search. Throws `Not implemented` for `similar`, and for `search` without the
|
|
31
|
+
* `search` option.
|
|
30
32
|
*/
|
|
31
|
-
const evaluateFilter$1 = (checked, record, options = {}) => evaluateFilter(lift(checked, { expand: true }), record, { functions: implementations(options.now ?? (() => /* @__PURE__ */ new Date())) });
|
|
33
|
+
const evaluateFilter$1 = (checked, record, options = {}) => evaluateFilter(lift(checked, { expand: true }), record, { functions: implementations(options.now ?? (() => /* @__PURE__ */ new Date()), record, options.search) });
|
|
32
34
|
//#endregion
|
|
33
35
|
export { evaluateFilter$1 as evaluateFilter };
|
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 { ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, RelatedModel, ResourceModel, SaveFeedback, SearchMatch, SearchResultModel, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
|
|
2
|
+
import { ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, 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";
|
|
@@ -36,4 +36,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
|
|
|
36
36
|
import { restrictModel } from "./access/restrict.js";
|
|
37
37
|
import "./access/index.js";
|
|
38
38
|
import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
|
|
39
|
-
export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, 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 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 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 Grant, type HeldCapability, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, 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 RecordOf, type Ref, type Refs, 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 SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SignInPromptModel, 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, resolveAccess, resource, restrictModel, rule, userMenu, view, where };
|
|
39
|
+
export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, 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 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 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 Grant, type HeldCapability, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, 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 RecordOf, type Ref, type Refs, 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 SignInPromptModel, 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, resolveAccess, resource, restrictModel, rule, userMenu, view, where };
|
package/dist/internal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
|
|
2
|
+
import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
|
|
2
3
|
import { checkEventFilter } from "./events/event-models.js";
|
|
3
4
|
import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
|
|
4
5
|
import { addressPlaceholders, appHrefError, fillAddress, hrefPlaceholders, leavesApp } from "./address-template.js";
|
|
@@ -8,7 +9,6 @@ import { bottomBarFor } from "./bottom-bar.js";
|
|
|
8
9
|
import { decodeKey, encodeKey } from "./keys.js";
|
|
9
10
|
import { webhookResources } from "./webhooks/webhook-resources.js";
|
|
10
11
|
import { pruneByShape, shapes } from "./ref-shape.js";
|
|
11
|
-
import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
|
|
12
12
|
import { inRange, pluginProblem } from "./plugin-version.js";
|
|
13
13
|
import { redactSecrets } from "./redact-secrets.js";
|
|
14
14
|
import { addressErrors } from "./address-errors.js";
|
package/dist/model.d.ts
CHANGED
|
@@ -338,7 +338,8 @@ export type CardFavoriteModel = {
|
|
|
338
338
|
};
|
|
339
339
|
/**
|
|
340
340
|
* What a visitor who is not signed in is told about a resource they could use once signed in (`.signIn(...)`): on its
|
|
341
|
-
* list page, in place of the list, and in the dialog of a card's favorite button
|
|
341
|
+
* list page, in place of the list, and in the dialog of a card's favorite button or a list's "Save search" button
|
|
342
|
+
* that saves to it. See
|
|
342
343
|
* [Sign-in prompts](website/src/content/docs/reference/ui-config.mdx#sign-in-prompts).
|
|
343
344
|
*/
|
|
344
345
|
export type SignInPromptModel = {
|
|
@@ -363,6 +364,44 @@ export type SignInViewModel = Pick<ViewModel, 'resource' | 'names' | 'nav'> & {
|
|
|
363
364
|
/** What the visitor is told. */
|
|
364
365
|
signIn: SignInPromptModel;
|
|
365
366
|
};
|
|
367
|
+
/**
|
|
368
|
+
* The "Save search" button of a list page: it saves the list's filter as a record of `resource`, the signed-in user's
|
|
369
|
+
* own, whose view lists them (`.searchesOf(...)`). A visitor who is not signed in gets the resource's sign-in prompt
|
|
370
|
+
* instead. See [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
|
|
371
|
+
*/
|
|
372
|
+
export type SaveSearchModel = {
|
|
373
|
+
/** The resource holding the saved searches, whose view has `searchesOf` for this one. */
|
|
374
|
+
resource: string;
|
|
375
|
+
/** The button's text, pressed while the filter is saved; "Save search" when omitted. */
|
|
376
|
+
label?: string;
|
|
377
|
+
};
|
|
378
|
+
/**
|
|
379
|
+
* What a saved search is (`.searchesOf(...)`): the list page shows each of the user's searches, with its filters, its
|
|
380
|
+
* `controls`, and the first records of `resource` it finds, newest first, and a link to all of them. See
|
|
381
|
+
* [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
|
|
382
|
+
*/
|
|
383
|
+
export type SearchesOfModel = {
|
|
384
|
+
/** The resource searched, such as listings. */
|
|
385
|
+
resource: string;
|
|
386
|
+
/** The text field holding the search's filter, as the list's address has it. */
|
|
387
|
+
filter: string;
|
|
388
|
+
/** The text field a new search is named in, from its filters; the view's title shows it. */
|
|
389
|
+
name?: string;
|
|
390
|
+
/** Enum and boolean fields changed in place on each search, such as how often to hear of new records. */
|
|
391
|
+
controls?: string[];
|
|
392
|
+
/**
|
|
393
|
+
* With it, each search shows only the records new to the user: those whose `field` (of `resource`) is after the
|
|
394
|
+
* search's `seenAt` (a timestamp field of this resource), which opening all of its results sets to now.
|
|
395
|
+
*/
|
|
396
|
+
unseen?: {
|
|
397
|
+
/** When the user last opened the search's results. */
|
|
398
|
+
seenAt: string;
|
|
399
|
+
/** The searched resource's timestamp that makes a record new, such as when it was published. */
|
|
400
|
+
field: string;
|
|
401
|
+
};
|
|
402
|
+
/** How many records each search shows; default 12. */
|
|
403
|
+
limit?: number;
|
|
404
|
+
};
|
|
366
405
|
/** A view's list page (`.list(...)`). */
|
|
367
406
|
export type ListModel = {
|
|
368
407
|
/** The table's columns, in order. */
|
|
@@ -716,8 +755,12 @@ export type ViewModel = {
|
|
|
716
755
|
actions: ActionModel[];
|
|
717
756
|
/** The relation field whose records the list page shows instead of this resource's rows, in their own view's list. */
|
|
718
757
|
listOf?: string;
|
|
719
|
-
/** What a visitor who is not signed in is told in place of the list, and by a
|
|
758
|
+
/** What a visitor who is not signed in is told in place of the list, and by a favorite or "Save search" button that saves here. */
|
|
720
759
|
signIn?: SignInPromptModel;
|
|
760
|
+
/** The list page's button that saves its filter as a search of the user's own. */
|
|
761
|
+
saveSearch?: SaveSearchModel;
|
|
762
|
+
/** The list page shows the user's saved searches of another resource, each with what it finds, instead of rows. */
|
|
763
|
+
searchesOf?: SearchesOfModel;
|
|
721
764
|
};
|
|
722
765
|
/** Where a part of a filter or order stands in its text, as character offsets. */
|
|
723
766
|
export type Span = {
|
package/dist/view.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionModel, ChartGranularity, ChartRange, FilterWidgetModel, LayoutItemModel, ListDisplayModel, NavModel, NavRecentModel, ResourceModel, SaveFeedback, SignInPromptModel, ViewModel } from "./model.js";
|
|
1
|
+
import { ActionModel, ChartGranularity, ChartRange, FilterWidgetModel, LayoutItemModel, ListDisplayModel, NavModel, NavRecentModel, 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
4
|
import { displays } from "./view-displays.js";
|
|
@@ -106,13 +106,39 @@ export declare class ViewBuilder<R extends {
|
|
|
106
106
|
listOf(pick: (r: FieldRefs<FieldsOf<R>>) => Ref): ViewBuilder<R>;
|
|
107
107
|
/**
|
|
108
108
|
* What a visitor who is not signed in is told about this resource, which they could use once signed in: on its list
|
|
109
|
-
* page, in place of the list, and in the dialog of a card's favorite button
|
|
110
|
-
* in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
|
|
109
|
+
* page, in place of the list, and in the dialog of a card's favorite button or a list's "Save search" button that
|
|
110
|
+
* saves here. The page offers "Sign in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
|
|
111
111
|
* bar and the sidebar of visitors. Throws when `signUp.href` is not `https:`, `http:` or `mailto:`. See
|
|
112
112
|
* [Sign-in prompts](website/src/content/docs/reference/ui-config.mdx#sign-in-prompts).
|
|
113
113
|
*/
|
|
114
114
|
signIn(prompt: SignInPromptModel): ViewBuilder<R>;
|
|
115
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* A "Save search" button on the list page while its filter narrows it: it saves the filter as a record of `resource`,
|
|
117
|
+
* the signed-in user's own, whose view lists them with `.searchesOf(...)`, and takes it off again while pressed. A
|
|
118
|
+
* visitor is told what signing in gives, when that view has `.signIn(...)`. See
|
|
119
|
+
* [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
|
|
120
|
+
*/
|
|
121
|
+
saveSearch(search: SaveSearchModel): ViewBuilder<R>;
|
|
122
|
+
/**
|
|
123
|
+
* This resource holds saved searches of `resource`, which a list page saves with `.saveSearch(...)`: its list page
|
|
124
|
+
* shows each of the user's searches with its filters, its `controls` to change in place, the newest records it finds
|
|
125
|
+
* (with `unseen`, only those new since the user last opened its results) and a link to all of them. Throws in
|
|
126
|
+
* `toModel` when `filter` or `name` is not a text field, `unseen.seenAt` not a timestamp or a control not an enum or
|
|
127
|
+
* boolean field.
|
|
128
|
+
* See [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
|
|
129
|
+
*/
|
|
130
|
+
searchesOf(pick: (r: FieldRefs<FieldsOf<R>>) => {
|
|
131
|
+
resource: string;
|
|
132
|
+
filter: Ref;
|
|
133
|
+
name?: Ref;
|
|
134
|
+
controls?: Ref[];
|
|
135
|
+
unseen?: {
|
|
136
|
+
seenAt: Ref;
|
|
137
|
+
field: string;
|
|
138
|
+
};
|
|
139
|
+
limit?: number;
|
|
140
|
+
}): ViewBuilder<R>;
|
|
141
|
+
/** With the resource's model, also verifies the title, search result and list display fields, `listOf`, `searchesOf` and the sidebar's `recent` group. */
|
|
116
142
|
toModel(resource?: ResourceModel): ViewModel;
|
|
117
143
|
private refs;
|
|
118
144
|
private next;
|
package/dist/view.js
CHANGED
|
@@ -6,6 +6,16 @@ import { displayErrors, displayFields, displays } from "./view-displays.js";
|
|
|
6
6
|
import { FieldView } from "./view-field.js";
|
|
7
7
|
import { widgets } from "./view-widgets.js";
|
|
8
8
|
//#region src/view.ts
|
|
9
|
+
const searchesOfErrors = (searches, resource) => {
|
|
10
|
+
const typed = (role, name, type) => name === void 0 || resource.fields[name]?.type === type ? [] : [`${role} "${name}" is not a ${type} field of the resource`];
|
|
11
|
+
return [
|
|
12
|
+
...typed("filter", searches.filter, "text"),
|
|
13
|
+
...typed("name", searches.name, "text"),
|
|
14
|
+
...typed("unseen.seenAt", searches.unseen?.seenAt, "timestamp"),
|
|
15
|
+
...(searches.controls ?? []).flatMap((name) => ["enum", "boolean"].includes(resource.fields[name]?.type ?? "") ? [] : [`control "${name}" is not an enum or boolean field of the resource`]),
|
|
16
|
+
...searches.limit === void 0 || Number.isInteger(searches.limit) && searches.limit > 0 ? [] : ["limit must be a positive whole number"]
|
|
17
|
+
];
|
|
18
|
+
};
|
|
9
19
|
/**
|
|
10
20
|
* A view of a resource, as `view(...)` starts it: how the admin shows it. Each method returns a new view, so one view
|
|
11
21
|
* can build on another. See [UI config](website/src/content/docs/reference/ui-config.mdx).
|
|
@@ -122,8 +132,8 @@ var ViewBuilder = class ViewBuilder {
|
|
|
122
132
|
}
|
|
123
133
|
/**
|
|
124
134
|
* What a visitor who is not signed in is told about this resource, which they could use once signed in: on its list
|
|
125
|
-
* page, in place of the list, and in the dialog of a card's favorite button
|
|
126
|
-
* in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
|
|
135
|
+
* page, in place of the list, and in the dialog of a card's favorite button or a list's "Save search" button that
|
|
136
|
+
* saves here. The page offers "Sign in", and a page to make an account when `signUp` names one. With it, the resource keeps its place in the bottom
|
|
127
137
|
* bar and the sidebar of visitors. Throws when `signUp.href` is not `https:`, `http:` or `mailto:`. See
|
|
128
138
|
* [Sign-in prompts](website/src/content/docs/reference/ui-config.mdx#sign-in-prompts).
|
|
129
139
|
*/
|
|
@@ -131,7 +141,39 @@ var ViewBuilder = class ViewBuilder {
|
|
|
131
141
|
if (prompt.signUp && !isMenuHref(prompt.signUp.href)) throw new Error(`View "${this.state.resource}": signIn signUp.href must start with https://, http:// or mailto:`);
|
|
132
142
|
return this.next({ signIn: structuredClone(prompt) });
|
|
133
143
|
}
|
|
134
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* A "Save search" button on the list page while its filter narrows it: it saves the filter as a record of `resource`,
|
|
146
|
+
* the signed-in user's own, whose view lists them with `.searchesOf(...)`, and takes it off again while pressed. A
|
|
147
|
+
* visitor is told what signing in gives, when that view has `.signIn(...)`. See
|
|
148
|
+
* [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
|
|
149
|
+
*/
|
|
150
|
+
saveSearch(search) {
|
|
151
|
+
return this.next({ saveSearch: { ...search } });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* This resource holds saved searches of `resource`, which a list page saves with `.saveSearch(...)`: its list page
|
|
155
|
+
* shows each of the user's searches with its filters, its `controls` to change in place, the newest records it finds
|
|
156
|
+
* (with `unseen`, only those new since the user last opened its results) and a link to all of them. Throws in
|
|
157
|
+
* `toModel` when `filter` or `name` is not a text field, `unseen.seenAt` not a timestamp or a control not an enum or
|
|
158
|
+
* boolean field.
|
|
159
|
+
* See [Saved searches](website/src/content/docs/reference/ui-config.mdx#saved-searches).
|
|
160
|
+
*/
|
|
161
|
+
searchesOf(pick) {
|
|
162
|
+
const { resource, filter, name, controls, unseen, limit } = pick(this.refs());
|
|
163
|
+
const searches = {
|
|
164
|
+
resource,
|
|
165
|
+
filter: filter.name,
|
|
166
|
+
...name && { name: name.name },
|
|
167
|
+
...controls && { controls: controls.map((control) => control.name) },
|
|
168
|
+
...unseen && { unseen: {
|
|
169
|
+
seenAt: unseen.seenAt.name,
|
|
170
|
+
field: unseen.field
|
|
171
|
+
} },
|
|
172
|
+
...limit !== void 0 && { limit }
|
|
173
|
+
};
|
|
174
|
+
return this.next({ searchesOf: searches });
|
|
175
|
+
}
|
|
176
|
+
/** With the resource's model, also verifies the title, search result and list display fields, `listOf`, `searchesOf` and the sidebar's `recent` group. */
|
|
135
177
|
toModel(resource) {
|
|
136
178
|
const { title, nav, searchResult, list, listOf } = this.state;
|
|
137
179
|
if (resource && title !== void 0 && !Object.hasOwn(resource.fields, title)) throw new Error(`View "${this.state.resource}": title field "${title}" does not exist on the resource`);
|
|
@@ -145,6 +187,8 @@ var ViewBuilder = class ViewBuilder {
|
|
|
145
187
|
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`);
|
|
146
188
|
}
|
|
147
189
|
if (resource && listOf !== void 0 && (!Object.hasOwn(resource.fields, listOf) || resource.fields[listOf].type !== "relation")) throw new Error(`View "${this.state.resource}": listOf names "${listOf}", which is not a relation field of the resource`);
|
|
190
|
+
const searchErrors = resource && this.state.searchesOf ? searchesOfErrors(this.state.searchesOf, resource) : [];
|
|
191
|
+
if (searchErrors.length > 0) throw new Error(`View "${this.state.resource}": searchesOf ${searchErrors.join("; ")}`);
|
|
148
192
|
const recentErrors = resource && nav?.recent ? navRecentErrors(nav.recent, resource) : [];
|
|
149
193
|
if (recentErrors.length > 0) throw new Error(`View "${this.state.resource}": nav.recent ${recentErrors.join("; ")}`);
|
|
150
194
|
return structuredClone(this.state);
|