@protobase/schema 0.21.6 → 0.21.8
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/access/role-entry.js +3 -3
- package/dist/access/roles.d.ts +2 -1
- package/dist/access/roles.js +2 -1
- package/dist/address-errors.js +1 -1
- package/dist/ai-prompt.d.ts +8 -0
- package/dist/ai-prompt.js +17 -0
- package/dist/app-address.d.ts +2 -0
- package/dist/app-address.js +22 -0
- package/dist/field.d.ts +12 -0
- package/dist/field.js +10 -0
- package/dist/form-address.d.ts +13 -0
- package/dist/form-address.js +15 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.js +4 -3
- package/dist/layout.d.ts +8 -0
- package/dist/layout.js +10 -0
- package/dist/model.d.ts +78 -4
- package/dist/resource-model.d.ts +1 -0
- package/dist/resource-model.js +18 -1
- package/dist/resource.d.ts +6 -0
- package/dist/resource.js +8 -0
- package/dist/view-actions.d.ts +4 -0
- package/dist/view-actions.js +14 -1
- package/dist/view-displays.d.ts +20 -1
- package/dist/view-displays.js +39 -2
- package/dist/view-form.d.ts +16 -0
- package/dist/view-form.js +22 -0
- package/dist/view.d.ts +11 -2
- package/dist/view.js +20 -3
- package/package.json +1 -1
|
@@ -33,10 +33,10 @@ const limited = (role, capability, limit) => {
|
|
|
33
33
|
const checkAnonymous = (held) => {
|
|
34
34
|
for (const { capability, fields } of held) {
|
|
35
35
|
const { name, action, scope } = parseCapability(capability);
|
|
36
|
-
if (action !== "read") throw new Error(`The anonymous role may only read, but it grants "${capability}"`);
|
|
37
|
-
if (name === "*") throw new Error(`The anonymous role must name each resource it
|
|
36
|
+
if (action !== "read" && action !== "create") throw new Error(`The anonymous role may only read, or create through a public form, but it grants "${capability}"`);
|
|
37
|
+
if (name === "*") throw new Error(`The anonymous role must name each resource it ${action}s, not "${capability}"`);
|
|
38
38
|
if (scope !== void 0) throw new Error(`The anonymous role grants "${capability}", but an anonymous caller owns no rows`);
|
|
39
|
-
if (!fields) throw new Error(`The anonymous role must list the fields it
|
|
39
|
+
if (!fields) throw new Error(`The anonymous role must list the fields it ${action}s: { '${capability}': ['name', ...] }`);
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
/** A role's entries as the capabilities it holds; throws on anything that is not a capability or a valid limit. */
|
package/dist/access/roles.d.ts
CHANGED
|
@@ -22,7 +22,8 @@ export type RoleLimit = HeldCapability & {
|
|
|
22
22
|
*
|
|
23
23
|
* An entry may limit capabilities to fields (an allowlist) and rows (an AIP-160 filter):
|
|
24
24
|
* `{ 'products.read': ['sku', 'price'] }`, `{ 'products.read': { fields: ['name'], filter: 'published = true' } }`.
|
|
25
|
-
* The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields
|
|
25
|
+
* The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields, and
|
|
26
|
+
* create listed fields through a public form.
|
|
26
27
|
* See [Access](website/src/content/docs/reference/access.mdx#capabilities).
|
|
27
28
|
*
|
|
28
29
|
* @example
|
package/dist/access/roles.js
CHANGED
|
@@ -11,7 +11,8 @@ const rolesOf = (user) => user.roles ?? [];
|
|
|
11
11
|
*
|
|
12
12
|
* An entry may limit capabilities to fields (an allowlist) and rows (an AIP-160 filter):
|
|
13
13
|
* `{ 'products.read': ['sku', 'price'] }`, `{ 'products.read': { fields: ['name'], filter: 'published = true' } }`.
|
|
14
|
-
* The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields
|
|
14
|
+
* The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields, and
|
|
15
|
+
* create listed fields through a public form.
|
|
15
16
|
* See [Access](website/src/content/docs/reference/access.mdx#capabilities).
|
|
16
17
|
*
|
|
17
18
|
* @example
|
package/dist/address-errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { addressPlaceholders } from "./address-template.js";
|
|
1
2
|
import { parseFilter } from "./filter/parse.js";
|
|
2
3
|
import { checkFilter } from "./filter/check.js";
|
|
3
4
|
import { checkOrderBy, parseOrderBy } from "./filter/order-by.js";
|
|
4
|
-
import { addressPlaceholders } from "./address-template.js";
|
|
5
5
|
//#region src/address-errors.ts
|
|
6
6
|
const nameErrors = [
|
|
7
7
|
"unknown-field",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/ai-prompt.d.ts
|
|
2
|
+
/** The fields an AI prompt reads from the record, from its `{field}` placeholders. */
|
|
3
|
+
export declare const promptFields: (prompt: string) => string[];
|
|
4
|
+
/** `prompt` with every `{field}` replaced by the record's value as text; a missing value becomes empty, json as JSON. */
|
|
5
|
+
export declare const renderPrompt: (prompt: string, record: Record<string, unknown>) => string;
|
|
6
|
+
/** Why a prompt cannot be an AI field's or action's: it reads no field of the record. */
|
|
7
|
+
export declare const promptError: (prompt: string) => "its prompt must name at least one {field} of the record" | undefined;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { hrefPlaceholders } from "./address-template.js";
|
|
2
|
+
//#region src/ai-prompt.ts
|
|
3
|
+
const placeholder = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
4
|
+
/** The fields an AI prompt reads from the record, from its `{field}` placeholders. */
|
|
5
|
+
const promptFields = (prompt) => hrefPlaceholders(prompt);
|
|
6
|
+
const asText = (value) => {
|
|
7
|
+
if (value === null || value === void 0) return "";
|
|
8
|
+
if (typeof value === "string") return value;
|
|
9
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
};
|
|
12
|
+
/** `prompt` with every `{field}` replaced by the record's value as text; a missing value becomes empty, json as JSON. */
|
|
13
|
+
const renderPrompt = (prompt, record) => prompt.replace(placeholder, (_, name) => asText(record[name]));
|
|
14
|
+
/** Why a prompt cannot be an AI field's or action's: it reads no field of the record. */
|
|
15
|
+
const promptError = (prompt) => promptFields(prompt).length === 0 ? "its prompt must name at least one {field} of the record" : void 0;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { promptError, promptFields, renderPrompt };
|
package/dist/app-address.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare const addressWords: {
|
|
|
15
15
|
readonly filterLayout: "layout";
|
|
16
16
|
/** Where a list's map display looks: `latitude,longitude,zoom`. */
|
|
17
17
|
readonly map: "map";
|
|
18
|
+
/** The month or week a list's calendar display shows: `month,YYYY-MM-DD` or `week,YYYY-MM-DD`. */
|
|
19
|
+
readonly calendar: "calendar";
|
|
18
20
|
/** The saved search a list was opened from, by its key. */
|
|
19
21
|
readonly savedSearch: "saved_search";
|
|
20
22
|
/** The second path segment that opens a resource's create form instead of a record. */
|
package/dist/app-address.js
CHANGED
|
@@ -15,6 +15,8 @@ const addressWords = {
|
|
|
15
15
|
filterLayout: "layout",
|
|
16
16
|
/** Where a list's map display looks: `latitude,longitude,zoom`. */
|
|
17
17
|
map: "map",
|
|
18
|
+
/** The month or week a list's calendar display shows: `month,YYYY-MM-DD` or `week,YYYY-MM-DD`. */
|
|
19
|
+
calendar: "calendar",
|
|
18
20
|
/** The saved search a list was opened from, by its key. */
|
|
19
21
|
savedSearch: "saved_search",
|
|
20
22
|
/** The second path segment that opens a resource's create form instead of a record. */
|
|
@@ -41,6 +43,23 @@ const readMap = (text) => {
|
|
|
41
43
|
zoom
|
|
42
44
|
} : void 0;
|
|
43
45
|
};
|
|
46
|
+
const calendarText = (view) => `${view.scale},${view.day}`;
|
|
47
|
+
const isDay = (text) => {
|
|
48
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
49
|
+
if (!match) return false;
|
|
50
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
51
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
52
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
53
|
+
};
|
|
54
|
+
const readCalendar = (text) => {
|
|
55
|
+
const parts = text?.split(",");
|
|
56
|
+
if (parts?.length !== 2) return void 0;
|
|
57
|
+
const [scale, day] = parts;
|
|
58
|
+
return (scale === "month" || scale === "week") && isDay(day) ? {
|
|
59
|
+
scale,
|
|
60
|
+
day
|
|
61
|
+
} : void 0;
|
|
62
|
+
};
|
|
44
63
|
const listQuery = (address) => {
|
|
45
64
|
const query = new URLSearchParams();
|
|
46
65
|
for (const name of listWords) {
|
|
@@ -48,6 +67,7 @@ const listQuery = (address) => {
|
|
|
48
67
|
if (value !== void 0 && value !== "") query.set(addressWords[name], value);
|
|
49
68
|
}
|
|
50
69
|
if (address.map) query.set(addressWords.map, mapText(address.map));
|
|
70
|
+
if (address.calendar) query.set(addressWords.calendar, calendarText(address.calendar));
|
|
51
71
|
const text = query.toString();
|
|
52
72
|
return text ? `?${text}` : "";
|
|
53
73
|
};
|
|
@@ -107,6 +127,7 @@ const readAppPath = (pathAndQuery, pages) => {
|
|
|
107
127
|
const orderBy = params.get(addressWords.orderBy) || void 0;
|
|
108
128
|
const layout = filterLayout(params.get(addressWords.filterLayout));
|
|
109
129
|
const map = readMap(params.get(addressWords.map));
|
|
130
|
+
const calendar = readCalendar(params.get(addressWords.calendar));
|
|
110
131
|
const savedSearch = params.get(addressWords.savedSearch) || void 0;
|
|
111
132
|
return {
|
|
112
133
|
kind: "list",
|
|
@@ -116,6 +137,7 @@ const readAppPath = (pathAndQuery, pages) => {
|
|
|
116
137
|
...orderBy && { orderBy },
|
|
117
138
|
...layout && { filterLayout: layout },
|
|
118
139
|
...map && { map },
|
|
140
|
+
...calendar && { calendar },
|
|
119
141
|
...savedSearch && { savedSearch }
|
|
120
142
|
};
|
|
121
143
|
};
|
package/dist/field.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export type FieldMeta = {
|
|
|
57
57
|
};
|
|
58
58
|
/** A file field's settings (`f.file()`). */
|
|
59
59
|
file?: FileOptions;
|
|
60
|
+
/** How a job fills it (`.ai()`). */
|
|
61
|
+
ai?: {
|
|
62
|
+
/** The prompt the job fills it from. */
|
|
63
|
+
prompt: string;
|
|
64
|
+
};
|
|
60
65
|
};
|
|
61
66
|
/** A field type: its name, the constraints it takes and how it builds the schema that checks a value. */
|
|
62
67
|
export type FieldTypeDef = {
|
|
@@ -115,6 +120,13 @@ export declare class Field<V> {
|
|
|
115
120
|
min(value: number): Field<V>;
|
|
116
121
|
/** The greatest value of an integer, or the longest length of a text. Only `f.integer()` and `f.text()` take it. */
|
|
117
122
|
max(value: number): Field<V>;
|
|
123
|
+
/**
|
|
124
|
+
* A job fills the field from `prompt`, `{field}` replaced by the record's values, after a create, and after an update
|
|
125
|
+
* that changes a field the prompt names, as the user who wrote; see [AI fields and actions](website/src/content/docs/reference/ai.mdx).
|
|
126
|
+
*/
|
|
127
|
+
ai(o: {
|
|
128
|
+
prompt: string;
|
|
129
|
+
}): Field<V>;
|
|
118
130
|
/** A copy of the field with `patch` applied, which every option returns. */
|
|
119
131
|
protected with(patch: Partial<FieldMeta>): Field<V>;
|
|
120
132
|
private constrain;
|
package/dist/field.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { promptError } from "./ai-prompt.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
//#region src/field.ts
|
|
3
4
|
const fieldRegistry = z.registry();
|
|
@@ -85,6 +86,15 @@ var Field = class Field {
|
|
|
85
86
|
max(value) {
|
|
86
87
|
return this.constrain("max", { max: value });
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* A job fills the field from `prompt`, `{field}` replaced by the record's values, after a create, and after an update
|
|
91
|
+
* that changes a field the prompt names, as the user who wrote; see [AI fields and actions](website/src/content/docs/reference/ai.mdx).
|
|
92
|
+
*/
|
|
93
|
+
ai(o) {
|
|
94
|
+
const error = promptError(o.prompt);
|
|
95
|
+
if (error) throw new Error(`.ai(): ${error}`);
|
|
96
|
+
return this.with({ ai: { prompt: o.prompt } });
|
|
97
|
+
}
|
|
88
98
|
/** A copy of the field with `patch` applied, which every option returns. */
|
|
89
99
|
with(patch) {
|
|
90
100
|
return new Field(this.def, {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/form-address.d.ts
|
|
2
|
+
/** The path of the public forms, below the app's base path: a form is at `/-/forms/<name>`. */
|
|
3
|
+
export declare const formRoute = "/-/forms";
|
|
4
|
+
/** Where a public form is: the form's name. */
|
|
5
|
+
export type FormAddress = {
|
|
6
|
+
/** The form's name, as `.form(...)` gives it. */
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
/** The path below the base path of the public form `name`. */
|
|
10
|
+
export declare const formPath: (name: string) => string;
|
|
11
|
+
/** The public form the path below the base path shows; `undefined` for any other path. */
|
|
12
|
+
export declare const readFormAddress: (path: string) => FormAddress | undefined;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/form-address.ts
|
|
2
|
+
/** The path of the public forms, below the app's base path: a form is at `/-/forms/<name>`. */
|
|
3
|
+
const formRoute = "/-/forms";
|
|
4
|
+
/** What a form's name may hold, since it is its address: lowercase letters, digits, `-` and `_`. */
|
|
5
|
+
const formNamePattern = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6
|
+
/** The path below the base path of the public form `name`. */
|
|
7
|
+
const formPath = (name) => `${formRoute}/${name}`;
|
|
8
|
+
/** The public form the path below the base path shows; `undefined` for any other path. */
|
|
9
|
+
const readFormAddress = (path) => {
|
|
10
|
+
if (!path.startsWith(`/-/forms/`)) return void 0;
|
|
11
|
+
const name = path.slice(9);
|
|
12
|
+
return formNamePattern.test(name) ? { name } : void 0;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { formNamePattern, formPath, formRoute, readFormAddress };
|
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, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
|
|
2
|
+
import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CalendarView, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CommentsSectionModel, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, FormModel, GalleryModel, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
|
|
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";
|
|
@@ -10,6 +10,7 @@ import { CreatedOptions, DeletedOptions, EventActor, EventContext, EventHelpers,
|
|
|
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
12
|
import { ChartInput, GroupChartInput, PeriodChartInput } from "./view-chart.js";
|
|
13
|
+
import { FormInput } from "./view-form.js";
|
|
13
14
|
import { ListColumn } from "./view-columns.js";
|
|
14
15
|
import { ComponentRef, ViewProps } from "./view-displays.js";
|
|
15
16
|
import { FieldView } from "./view-field.js";
|
|
@@ -22,6 +23,7 @@ import { BottomBarNames, bottomBarErrors } from "./bottom-bar-errors.js";
|
|
|
22
23
|
import { layout } from "./layout.js";
|
|
23
24
|
import { KeyType, fitsKeyType, keyTypes } from "./keys.js";
|
|
24
25
|
import { addressWords, appPath, readAppPath } from "./app-address.js";
|
|
26
|
+
import { FormAddress, formPath, formRoute, readFormAddress } from "./form-address.js";
|
|
25
27
|
import { SignInAddress, SignInStep, readSignInAddress, signInParams, signInPath, signInRoute, signInSteps } from "./sign-in-address.js";
|
|
26
28
|
import { FilterLimits, FilterParse, parseFilter } from "./filter/parse.js";
|
|
27
29
|
import { CheckResult, checkFilter } from "./filter/check.js";
|
|
@@ -39,4 +41,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
|
|
|
39
41
|
import { restrictModel } from "./access/restrict.js";
|
|
40
42
|
import "./access/index.js";
|
|
41
43
|
import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
|
|
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 HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
|
|
44
|
+
export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type CalendarView, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CommentsSectionModel, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type FormAddress, type FormInput, type FormModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createWhere, where } from "./filter/where.js";
|
|
|
9
9
|
import { anonymousRole } from "./access/role-entry.js";
|
|
10
10
|
import { accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
|
|
11
11
|
import { resource } from "./resource.js";
|
|
12
|
+
import { formPath, formRoute, readFormAddress } from "./form-address.js";
|
|
12
13
|
import { fieldRef } from "./field-ref.js";
|
|
13
14
|
import { view } from "./view.js";
|
|
14
15
|
import { enumLabel } from "./enum-label.js";
|
|
@@ -23,4 +24,4 @@ import { rule } from "./access/rule.js";
|
|
|
23
24
|
import { defineRoles } from "./access/roles.js";
|
|
24
25
|
import { pickView } from "./access/pick-view.js";
|
|
25
26
|
import { restrictModel } from "./access/restrict.js";
|
|
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 };
|
|
27
|
+
export { accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
|
|
2
2
|
import { actionInputErrors, shownInputs } from "./action-inputs.js";
|
|
3
3
|
import { chartFields, isGroupChart } from "./view-chart.js";
|
|
4
|
-
import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
|
|
4
|
+
import { boardErrors, calendarErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
|
|
5
5
|
import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
|
|
6
6
|
import { bottomBarFor } from "./bottom-bar.js";
|
|
7
7
|
import { decodeKey, encodeKey } from "./keys.js";
|
|
@@ -13,7 +13,8 @@ import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.j
|
|
|
13
13
|
import { PluginHost, PluginRequirement, inRange, pluginProblem } from "./plugin-version.js";
|
|
14
14
|
import { redactSecrets } from "./redact-secrets.js";
|
|
15
15
|
import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
|
|
16
|
+
import { promptError, promptFields, renderPrompt } from "./ai-prompt.js";
|
|
16
17
|
import { AddressCheck, addressErrors } from "./address-errors.js";
|
|
17
18
|
import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
|
|
18
19
|
import { readEventStream } from "./assistant/event-stream.js";
|
|
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 };
|
|
20
|
+
export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
|
package/dist/internal.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
|
|
2
|
+
import { promptError, promptFields, renderPrompt } from "./ai-prompt.js";
|
|
1
3
|
import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
|
|
2
4
|
import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
|
|
3
5
|
import { checkEventFilter } from "./events/event-models.js";
|
|
4
6
|
import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
|
|
5
7
|
import { actionInputErrors, shownInputs } from "./action-inputs.js";
|
|
6
|
-
import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
|
|
7
8
|
import { chartFields, isGroupChart } from "./view-chart.js";
|
|
8
9
|
import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
|
|
9
|
-
import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
|
|
10
|
+
import { boardErrors, calendarErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
|
|
10
11
|
import { bottomBarFor } from "./bottom-bar.js";
|
|
11
12
|
import { decodeKey, encodeKey } from "./keys.js";
|
|
12
13
|
import { webhookResources } from "./webhooks/webhook-resources.js";
|
|
@@ -16,4 +17,4 @@ import { redactSecrets } from "./redact-secrets.js";
|
|
|
16
17
|
import { addressErrors } from "./address-errors.js";
|
|
17
18
|
import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
|
|
18
19
|
import { readEventStream } from "./assistant/event-stream.js";
|
|
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 };
|
|
20
|
+
export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
|
package/dist/layout.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ export declare const layout: {
|
|
|
73
73
|
limit?: number;
|
|
74
74
|
when?: ShowWhen;
|
|
75
75
|
}) => LayoutItemModel;
|
|
76
|
+
/**
|
|
77
|
+
* The record's comments, oldest first, with who wrote each and when, and a box to write one; `@` mentions someone who
|
|
78
|
+
* may read the record. Needs `.comments()` on the resource; a view without a layout shows it last by itself. See
|
|
79
|
+
* [Record comments](website/src/content/docs/reference/comments.mdx).
|
|
80
|
+
*/
|
|
81
|
+
comments: (title?: string, o?: {
|
|
82
|
+
when?: ShowWhen;
|
|
83
|
+
}) => LayoutItemModel;
|
|
76
84
|
/**
|
|
77
85
|
* A component of the project, registered in `protobase.ui.tsx` under its name, drawn where it is listed: in the main
|
|
78
86
|
* column, or in the sidebar with `sidebar: true`. The record page stays around it, with its title, breadcrumbs,
|
package/dist/layout.js
CHANGED
|
@@ -94,6 +94,16 @@ const layout = {
|
|
|
94
94
|
};
|
|
95
95
|
},
|
|
96
96
|
/**
|
|
97
|
+
* The record's comments, oldest first, with who wrote each and when, and a box to write one; `@` mentions someone who
|
|
98
|
+
* may read the record. Needs `.comments()` on the resource; a view without a layout shows it last by itself. See
|
|
99
|
+
* [Record comments](website/src/content/docs/reference/comments.mdx).
|
|
100
|
+
*/
|
|
101
|
+
comments: (title = "Comments", o = {}) => ({
|
|
102
|
+
kind: "comments",
|
|
103
|
+
title,
|
|
104
|
+
...o.when !== void 0 && { when: o.when }
|
|
105
|
+
}),
|
|
106
|
+
/**
|
|
97
107
|
* A component of the project, registered in `protobase.ui.tsx` under its name, drawn where it is listed: in the main
|
|
98
108
|
* column, or in the sidebar with `sidebar: true`. The record page stays around it, with its title, breadcrumbs,
|
|
99
109
|
* actions and save; the component reads the record with `useLayoutRecord()`.
|
package/dist/model.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export type FieldModel = {
|
|
|
44
44
|
};
|
|
45
45
|
/** A file field's allowed types (empty: any), largest upload in bytes, provider, and what each upload writes. */
|
|
46
46
|
file?: FileFieldModel;
|
|
47
|
+
/** A job fills it from the prompt after a create or an update of a field the prompt names (`.ai()`). */
|
|
48
|
+
ai?: {
|
|
49
|
+
/** The prompt, with `{field}` placeholders. */
|
|
50
|
+
prompt: string;
|
|
51
|
+
};
|
|
47
52
|
};
|
|
48
53
|
/** A file field's settings, as `/meta` gives them; see [Files](website/src/content/docs/reference/files.mdx). */
|
|
49
54
|
export type FileFieldModel = {
|
|
@@ -105,6 +110,8 @@ export type ResourceModel = {
|
|
|
105
110
|
events?: EventModel[];
|
|
106
111
|
/** Whether every change of its records is kept, with who made it (`.history()`). */
|
|
107
112
|
history?: true;
|
|
113
|
+
/** Whether people who may read its records comment on them (`.comments()`). */
|
|
114
|
+
comments?: true;
|
|
108
115
|
};
|
|
109
116
|
/** What happened to a record: `delete` and `undelete` include setting and clearing a soft delete field. */
|
|
110
117
|
export type EventOperation = 'create' | 'update' | 'delete' | 'undelete';
|
|
@@ -164,11 +171,18 @@ export type MapView = {
|
|
|
164
171
|
/** MapLibre's zoom, from 0 to 22. */
|
|
165
172
|
zoom: number;
|
|
166
173
|
};
|
|
174
|
+
/** Which month or week a list's calendar display shows: the grid's `scale`, and a UTC day (`YYYY-MM-DD`) in it. */
|
|
175
|
+
export type CalendarView = {
|
|
176
|
+
/** A month of whole weeks, or one week. */
|
|
177
|
+
scale: 'month' | 'week';
|
|
178
|
+
/** A day in the month or week, as `YYYY-MM-DD`. */
|
|
179
|
+
day: string;
|
|
180
|
+
};
|
|
167
181
|
/**
|
|
168
182
|
* A place in the app: a resource's list, one of its records, its create form, or a composed page. A record's `key` is
|
|
169
183
|
* its one value or its parts, as the record API takes them; `readAppPath` gives it back as the API's text form. A create
|
|
170
184
|
* form's `values` fill in fields of the new record by name, as text (`{ listing_id: '42' }`). A list's `map` is where its
|
|
171
|
-
* map display looks. See [Addresses](website/src/content/docs/reference/layouts.md#addresses).
|
|
185
|
+
* map display looks, and its `calendar` the month or week its calendar display shows. See [Addresses](website/src/content/docs/reference/layouts.md#addresses).
|
|
172
186
|
*/
|
|
173
187
|
export type AppAddress = {
|
|
174
188
|
/** A resource's list. */
|
|
@@ -185,6 +199,8 @@ export type AppAddress = {
|
|
|
185
199
|
filterLayout?: FilterLayout;
|
|
186
200
|
/** Where its map display looks. */
|
|
187
201
|
map?: MapView;
|
|
202
|
+
/** The month or week its calendar display shows. */
|
|
203
|
+
calendar?: CalendarView;
|
|
188
204
|
/**
|
|
189
205
|
* The saved search the list was opened from, by its key in the text form the record API takes: while the list's
|
|
190
206
|
* filter differs from the search's, the list offers to update the search.
|
|
@@ -365,6 +381,21 @@ export type ListDisplayModel = {
|
|
|
365
381
|
badge?: string;
|
|
366
382
|
/** A few small fields, such as dates, each shown when it has a value. */
|
|
367
383
|
meta?: string[];
|
|
384
|
+
} | {
|
|
385
|
+
/** The records on the days they cover, in a month grid or a week grid, moved to another day by dragging. */
|
|
386
|
+
kind: 'calendar';
|
|
387
|
+
/** What the address names (`?display=calendar`). */
|
|
388
|
+
name: string;
|
|
389
|
+
/** The display's name in the switch. */
|
|
390
|
+
label?: string;
|
|
391
|
+
/** The display's icon in the switch. */
|
|
392
|
+
icon?: string;
|
|
393
|
+
/** The filterable date or timestamp field a record starts on. */
|
|
394
|
+
start: string;
|
|
395
|
+
/** The filterable field of `start`'s type a record ends on; a record is on its start day alone when omitted. */
|
|
396
|
+
end?: string;
|
|
397
|
+
/** The event's text; the view's title when omitted. */
|
|
398
|
+
title?: string;
|
|
368
399
|
} | {
|
|
369
400
|
/** The rows drawn by a project component. */
|
|
370
401
|
kind: 'custom';
|
|
@@ -597,6 +628,11 @@ export type RelatedModel = {
|
|
|
597
628
|
sort?: string;
|
|
598
629
|
/** Fields of `resource`, through relations too (`unitId.propertyId`). */
|
|
599
630
|
columns?: string[];
|
|
631
|
+
/**
|
|
632
|
+
* The relation field of `resource` whose record a row opens, for rows that are links between this record and another
|
|
633
|
+
* (a join resource); by default the one relation in `resource`'s primary key besides `field`, when there is exactly one.
|
|
634
|
+
*/
|
|
635
|
+
open?: string;
|
|
600
636
|
/** How many records it shows, the first in `sort` order; with more, it says so ("The first 10 of 37.") beside "View all", which opens the rest in the list. */
|
|
601
637
|
limit?: number;
|
|
602
638
|
/** The display of `resource`'s list it opens in, such as `cards`; the list's default when omitted. */
|
|
@@ -659,6 +695,18 @@ export type HistorySectionModel = {
|
|
|
659
695
|
/** Whether it shows on the form, the read view or both. */
|
|
660
696
|
when?: ShowWhen;
|
|
661
697
|
};
|
|
698
|
+
/**
|
|
699
|
+
* The record's comments, oldest first, each with its author and time, and a box to write a new one that mentions people
|
|
700
|
+
* with `@`. Needs `.comments()` on the resource. See [Record comments](website/src/content/docs/reference/comments.mdx).
|
|
701
|
+
*/
|
|
702
|
+
export type CommentsSectionModel = {
|
|
703
|
+
/** A comments section. */
|
|
704
|
+
kind: 'comments';
|
|
705
|
+
/** The section's title. */
|
|
706
|
+
title: string;
|
|
707
|
+
/** Whether it shows on the form, the read view or both. */
|
|
708
|
+
when?: ShowWhen;
|
|
709
|
+
};
|
|
662
710
|
/** How a section shows its fields on the read view: `facts` as a row of key facts; label/value rows when omitted. */
|
|
663
711
|
export type SectionPresentation = 'facts';
|
|
664
712
|
/** An item of the record page (`.layout((r) => ...)` with `l`); see [The record page](website/src/content/docs/reference/ui-config.mdx#the-record-page). */
|
|
@@ -687,7 +735,7 @@ export type LayoutItemModel = {
|
|
|
687
735
|
fields: string[];
|
|
688
736
|
/** Whether it shows on the form, the read view or both. */
|
|
689
737
|
when?: ShowWhen;
|
|
690
|
-
} | RelatedModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel;
|
|
738
|
+
} | RelatedModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel | CommentsSectionModel;
|
|
691
739
|
/**
|
|
692
740
|
* A record's photos as tiles in the main column under `title`, one large and the next few smaller, opening a lightbox
|
|
693
741
|
* that steps through them all: first the record's own file field `image`, then the `image` of the records of
|
|
@@ -744,8 +792,8 @@ export type RecordMode = 'edit' | 'read' | 'locked';
|
|
|
744
792
|
export type SaveFeedback = 'toast' | 'button';
|
|
745
793
|
/**
|
|
746
794
|
* What a named action does in the app: set fields of the record (`update`), delete it (`delete`), go to an address in
|
|
747
|
-
* the app (`link` with `to`)
|
|
748
|
-
* values. Without it the app runs a handler it registered by name.
|
|
795
|
+
* the app (`link` with `to`), open a link that leaves it (`link` with `href`), `{field}` replaced by the record's
|
|
796
|
+
* values, or have the model write a field (`ai`). Without it the app runs a handler it registered by name.
|
|
749
797
|
*/
|
|
750
798
|
export type ActionRun = {
|
|
751
799
|
/** Updates the record. */
|
|
@@ -768,6 +816,13 @@ export type ActionRun = {
|
|
|
768
816
|
* the `https:` or `http:` URL the record holds, without which the action does not show.
|
|
769
817
|
*/
|
|
770
818
|
href: string;
|
|
819
|
+
} | {
|
|
820
|
+
/** Has the model write one field of the record from `prompt`, `{field}` replaced by the record's values, as the user. */
|
|
821
|
+
kind: 'ai';
|
|
822
|
+
/** The prompt, with `{field}` placeholders; `/meta` leaves it out. */
|
|
823
|
+
prompt?: string;
|
|
824
|
+
/** The field it writes. */
|
|
825
|
+
writes: string;
|
|
771
826
|
};
|
|
772
827
|
/** A named action of a view (`.actions((a) => ...)`); see [Named actions](website/src/content/docs/reference/layouts.md#named-actions). */
|
|
773
828
|
export type ActionModel = {
|
|
@@ -1000,6 +1055,25 @@ export type ViewModel = {
|
|
|
1000
1055
|
export?: false | ExportModel;
|
|
1001
1056
|
/** The page that creates the resource's records, in place of the create form. */
|
|
1002
1057
|
create?: CreateModel;
|
|
1058
|
+
/** Public forms that create its records for people without an account, each at `/-/forms/<name>`. */
|
|
1059
|
+
forms?: FormModel[];
|
|
1060
|
+
};
|
|
1061
|
+
/**
|
|
1062
|
+
* A public form (`.form(...)`): anyone, signed in or not, opens it at `/-/forms/<name>` and creates a record with its
|
|
1063
|
+
* fields, through the `anonymous` role's `create`, behind a rate limit and a bot check. See
|
|
1064
|
+
* [Public forms](website/src/content/docs/reference/forms.mdx).
|
|
1065
|
+
*/
|
|
1066
|
+
export type FormModel = {
|
|
1067
|
+
/** Its address in the app, `/-/forms/<name>`; unique across the app. */
|
|
1068
|
+
name: string;
|
|
1069
|
+
/** The page's heading; the view's singular name when omitted. */
|
|
1070
|
+
title?: string;
|
|
1071
|
+
/** The fields a visitor fills in, in order. */
|
|
1072
|
+
fields: string[];
|
|
1073
|
+
/** What the page shows once the record is created. */
|
|
1074
|
+
thanks: string;
|
|
1075
|
+
/** How many submissions one client may send per minute. */
|
|
1076
|
+
perMinute: number;
|
|
1003
1077
|
};
|
|
1004
1078
|
/** How the resource's records are created (`.create(...)`): a page of the app's own. */
|
|
1005
1079
|
export type CreateModel = {
|
package/dist/resource-model.d.ts
CHANGED
package/dist/resource-model.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { promptFields } from "./ai-prompt.js";
|
|
1
2
|
import { buildEventModels } from "./events/event-models.js";
|
|
2
3
|
import { snakeCase } from "./naming.js";
|
|
3
4
|
//#region src/resource-model.ts
|
|
@@ -40,7 +41,8 @@ const fieldModel = (name, field) => {
|
|
|
40
41
|
...meta.file.variants && Object.keys(meta.file.variants).length > 0 && { variants: structuredClone(meta.file.variants) },
|
|
41
42
|
...meta.file.original && { original: { ...meta.file.original } },
|
|
42
43
|
...meta.file.external && { external: true }
|
|
43
|
-
} }
|
|
44
|
+
} },
|
|
45
|
+
...meta.ai && { ai: { prompt: meta.ai.prompt } }
|
|
44
46
|
};
|
|
45
47
|
};
|
|
46
48
|
const assembleModel = (state) => {
|
|
@@ -59,6 +61,7 @@ const assembleModel = (state) => {
|
|
|
59
61
|
...state.searchMatch && Object.keys(state.searchMatch).length > 0 && { searchMatch: { ...state.searchMatch } },
|
|
60
62
|
...state.owner && { owner: state.owner },
|
|
61
63
|
...state.history && { history: true },
|
|
64
|
+
...state.comments && { comments: true },
|
|
62
65
|
...state.position && { position: { ...state.position } }
|
|
63
66
|
};
|
|
64
67
|
};
|
|
@@ -118,6 +121,19 @@ const positionProblems = (model) => {
|
|
|
118
121
|
...parent && !parent.filterable ? [`position "${name}" is within "${within}", which is not filterable, and each parent's records are found by it`] : []
|
|
119
122
|
];
|
|
120
123
|
};
|
|
124
|
+
const aiProblems = (model) => Object.values(model.fields).filter((field) => field.ai).flatMap((field) => [
|
|
125
|
+
...field.type === "text" || field.type === "enum" ? [] : [`is ${field.type}, not text or an enum`],
|
|
126
|
+
...field.nullable ? [] : ["must be optional(), empty until the job fills it"],
|
|
127
|
+
...field.readOnly ? ["cannot be readOnly(), as the job writes it through the API"] : [],
|
|
128
|
+
...field.sensitive ? ["cannot be sensitive"] : [],
|
|
129
|
+
...promptFields(field.ai.prompt).flatMap((name) => {
|
|
130
|
+
const read = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
131
|
+
if (!read) return [`reads "${name}", which is not a field`];
|
|
132
|
+
if (name === field.name) return [`reads "${name}", itself`];
|
|
133
|
+
if (read.ai) return [`reads "${name}", which is an AI field too`];
|
|
134
|
+
return read.sensitive ? [`reads "${name}", which is sensitive`] : [];
|
|
135
|
+
})
|
|
136
|
+
].map((problem) => `AI field "${field.name}" ${problem}`));
|
|
121
137
|
const buildResourceModel = (state) => {
|
|
122
138
|
const model = assembleModel(state);
|
|
123
139
|
const events = state.events && buildEventModels(model, state.events);
|
|
@@ -125,6 +141,7 @@ const buildResourceModel = (state) => {
|
|
|
125
141
|
...sensitiveProblems(model),
|
|
126
142
|
...fileProblems(model),
|
|
127
143
|
...positionProblems(model),
|
|
144
|
+
...aiProblems(model),
|
|
128
145
|
...events?.problems ?? []
|
|
129
146
|
];
|
|
130
147
|
if (problems.length > 0) throw new Error(`Resource "${model.name}": ${problems.join("; ")}`);
|
package/dist/resource.d.ts
CHANGED
|
@@ -103,6 +103,12 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
|
|
|
103
103
|
* [Record history](website/src/content/docs/reference/history.mdx).
|
|
104
104
|
*/
|
|
105
105
|
history(): ResourceBuilder<F, K>;
|
|
106
|
+
/**
|
|
107
|
+
* Lets the people who may read a record comment on it, and mention others who may read it too, who get an email. The
|
|
108
|
+
* record page shows the thread with `l.comments()`, and admins list every comment as Comments. See
|
|
109
|
+
* [Record comments](website/src/content/docs/reference/comments.mdx).
|
|
110
|
+
*/
|
|
111
|
+
comments(): ResourceBuilder<F, K>;
|
|
106
112
|
/** The code predicates (`match`) of its events, by event name. */
|
|
107
113
|
eventMatches(): Record<string, EventMatch<any>>;
|
|
108
114
|
/**
|
package/dist/resource.js
CHANGED
|
@@ -120,6 +120,14 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
120
120
|
history() {
|
|
121
121
|
return this.next({ history: true });
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Lets the people who may read a record comment on it, and mention others who may read it too, who get an email. The
|
|
125
|
+
* record page shows the thread with `l.comments()`, and admins list every comment as Comments. See
|
|
126
|
+
* [Record comments](website/src/content/docs/reference/comments.mdx).
|
|
127
|
+
*/
|
|
128
|
+
comments() {
|
|
129
|
+
return this.next({ comments: true });
|
|
130
|
+
}
|
|
123
131
|
/** The code predicates (`match`) of its events, by event name. */
|
|
124
132
|
eventMatches() {
|
|
125
133
|
return eventMatches(this.toModel(), this.state.events ?? []);
|
package/dist/view-actions.d.ts
CHANGED
package/dist/view-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { actionInputErrors, inputs } from "./action-inputs.js";
|
|
2
1
|
import { linkActionError } from "./address-template.js";
|
|
2
|
+
import { promptError } from "./ai-prompt.js";
|
|
3
|
+
import { actionInputErrors, inputs } from "./action-inputs.js";
|
|
3
4
|
//#region src/view-actions.ts
|
|
4
5
|
const named = (name, o) => ({
|
|
5
6
|
name,
|
|
@@ -64,6 +65,18 @@ const actions = {
|
|
|
64
65
|
href: o.href
|
|
65
66
|
}
|
|
66
67
|
};
|
|
68
|
+
},
|
|
69
|
+
ai: (name, o) => {
|
|
70
|
+
const error = promptError(o.prompt) ?? (o.writes === "" ? "it writes no field" : void 0);
|
|
71
|
+
if (error) throw new Error(`Action "${name}": ${error}`);
|
|
72
|
+
return {
|
|
73
|
+
...named(name, o),
|
|
74
|
+
run: {
|
|
75
|
+
kind: "ai",
|
|
76
|
+
prompt: o.prompt,
|
|
77
|
+
writes: o.writes
|
|
78
|
+
}
|
|
79
|
+
};
|
|
67
80
|
}
|
|
68
81
|
};
|
|
69
82
|
//#endregion
|
package/dist/view-displays.d.ts
CHANGED
|
@@ -92,6 +92,21 @@ export declare const displays: {
|
|
|
92
92
|
badge?: Ref;
|
|
93
93
|
meta?: Ref[];
|
|
94
94
|
}, o?: Naming) => ListDisplayModel;
|
|
95
|
+
/**
|
|
96
|
+
* The records on the days from `start` to `end` (or on their start day alone), in a month grid of whole weeks or a
|
|
97
|
+
* week grid, with weeks from Monday and days in UTC. It loads only the days shown through the list API, with the
|
|
98
|
+
* list's filter and search, in start order. An event shows `title` (default: the view's title), after its time for a
|
|
99
|
+
* timestamp, and opens the record. Dragging an event to another day, or picking it up with Space and moving it with
|
|
100
|
+
* the arrow keys, saves the record with `start` and `end` moved by the same days, as one update with its ETag, so the
|
|
101
|
+
* resource's access rules and `validate` decide, and a refusal puts the event back and says why. `start` must be a
|
|
102
|
+
* filterable date or timestamp field and `end` a filterable field of the same type. Named `calendar` unless `name`
|
|
103
|
+
* says otherwise. See [Calendars](website/src/content/docs/reference/ui-config.mdx#calendars).
|
|
104
|
+
*/
|
|
105
|
+
calendar: (fields: {
|
|
106
|
+
start: Ref;
|
|
107
|
+
end?: Ref;
|
|
108
|
+
title?: Ref;
|
|
109
|
+
}, o?: Naming) => ListDisplayModel;
|
|
95
110
|
/**
|
|
96
111
|
* The list drawn by a component of the project, registered in `protobase.ui.tsx` under its name. It gets `props` and
|
|
97
112
|
* reads the rows with `useListDisplay()`; `fields` are the fields it shows, fetched with relation names.
|
|
@@ -107,6 +122,10 @@ export declare const displays: {
|
|
|
107
122
|
export declare const boardErrors: (display: Extract<ListDisplayModel, {
|
|
108
123
|
kind: "board";
|
|
109
124
|
}>, model: ResourceModel) => string[];
|
|
110
|
-
/**
|
|
125
|
+
/** What makes a calendar unable to place the records on days: a `start` that is no filterable date or timestamp, or an `end` that is no filterable field of its type. */
|
|
126
|
+
export declare const calendarErrors: (display: Extract<ListDisplayModel, {
|
|
127
|
+
kind: "calendar";
|
|
128
|
+
}>, model: ResourceModel) => string[];
|
|
129
|
+
/** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, a calendar's dates, and the field references in a custom display's props. */
|
|
111
130
|
export declare const displayFields: (display: ListDisplayModel) => string[];
|
|
112
131
|
//#endregion
|
package/dist/view-displays.js
CHANGED
|
@@ -92,6 +92,24 @@ const displays = {
|
|
|
92
92
|
...fields.meta && { meta: fields.meta.map((ref) => ref.name) }
|
|
93
93
|
}),
|
|
94
94
|
/**
|
|
95
|
+
* The records on the days from `start` to `end` (or on their start day alone), in a month grid of whole weeks or a
|
|
96
|
+
* week grid, with weeks from Monday and days in UTC. It loads only the days shown through the list API, with the
|
|
97
|
+
* list's filter and search, in start order. An event shows `title` (default: the view's title), after its time for a
|
|
98
|
+
* timestamp, and opens the record. Dragging an event to another day, or picking it up with Space and moving it with
|
|
99
|
+
* the arrow keys, saves the record with `start` and `end` moved by the same days, as one update with its ETag, so the
|
|
100
|
+
* resource's access rules and `validate` decide, and a refusal puts the event back and says why. `start` must be a
|
|
101
|
+
* filterable date or timestamp field and `end` a filterable field of the same type. Named `calendar` unless `name`
|
|
102
|
+
* says otherwise. See [Calendars](website/src/content/docs/reference/ui-config.mdx#calendars).
|
|
103
|
+
*/
|
|
104
|
+
calendar: (fields, o = {}) => ({
|
|
105
|
+
kind: "calendar",
|
|
106
|
+
name: o.name ?? "calendar",
|
|
107
|
+
...naming(o),
|
|
108
|
+
start: fields.start.name,
|
|
109
|
+
...fields.end && { end: fields.end.name },
|
|
110
|
+
...fields.title && { title: fields.title.name }
|
|
111
|
+
}),
|
|
112
|
+
/**
|
|
95
113
|
* The list drawn by a component of the project, registered in `protobase.ui.tsx` under its name. It gets `props` and
|
|
96
114
|
* reads the rows with `useListDisplay()`; `fields` are the fields it shows, fetched with relation names.
|
|
97
115
|
*/
|
|
@@ -131,7 +149,21 @@ const boardErrors = (display, model) => {
|
|
|
131
149
|
if (field.type !== "enum") return [`"${display.by}" on ${model.name} is ${field.type}, not an enum, whose values are the board's columns`];
|
|
132
150
|
return field.filterable ? [] : [`"${display.by}" on ${model.name} is not filterable, and each column of the board filters by its value`];
|
|
133
151
|
};
|
|
134
|
-
/**
|
|
152
|
+
/** What makes a calendar unable to place the records on days: a `start` that is no filterable date or timestamp, or an `end` that is no filterable field of its type. */
|
|
153
|
+
const calendarErrors = (display, model) => {
|
|
154
|
+
const known = (name) => Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
155
|
+
const start = known(display.start);
|
|
156
|
+
const end = display.end === void 0 ? void 0 : known(display.end);
|
|
157
|
+
if (!start) return [];
|
|
158
|
+
if (start.type !== "date" && start.type !== "timestamp") return [`start "${display.start}" on ${model.name} is ${start.type}, not a date or timestamp`];
|
|
159
|
+
const unfilterable = (name) => `${name === display.start ? "start" : "end"} "${name}" on ${model.name} is not filterable, and the calendar filters by it for the days it shows`;
|
|
160
|
+
return [
|
|
161
|
+
...start.filterable ? [] : [unfilterable(display.start)],
|
|
162
|
+
...end && end.type !== start.type ? [`end "${display.end}" on ${model.name} is ${end.type}, not ${start.type} like start "${display.start}"`] : [],
|
|
163
|
+
...end && end.type === start.type && !end.filterable ? [unfilterable(display.end)] : []
|
|
164
|
+
];
|
|
165
|
+
};
|
|
166
|
+
/** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, a calendar's dates, and the field references in a custom display's props. */
|
|
135
167
|
const displayFields = (display) => {
|
|
136
168
|
if (display.kind === "map") return [
|
|
137
169
|
display.latitude,
|
|
@@ -149,6 +181,11 @@ const displayFields = (display) => {
|
|
|
149
181
|
display.badge,
|
|
150
182
|
...display.meta ?? []
|
|
151
183
|
].filter((name) => name !== void 0);
|
|
184
|
+
if (display.kind === "calendar") return [
|
|
185
|
+
display.start,
|
|
186
|
+
display.end,
|
|
187
|
+
display.title
|
|
188
|
+
].filter((name) => name !== void 0);
|
|
152
189
|
return [
|
|
153
190
|
display.image,
|
|
154
191
|
display.title,
|
|
@@ -161,4 +198,4 @@ const displayFields = (display) => {
|
|
|
161
198
|
].filter((name) => name !== void 0);
|
|
162
199
|
};
|
|
163
200
|
//#endregion
|
|
164
|
-
export { boardErrors, cardPhotoLimit, customComponent, displayErrors, displayFields, displays, tableDisplay };
|
|
201
|
+
export { boardErrors, calendarErrors, cardPhotoLimit, customComponent, displayErrors, displayFields, displays, tableDisplay };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Ref } from "./refs.js";
|
|
2
|
+
//#region src/view-form.d.ts
|
|
3
|
+
/** What `.form(...)` takes. */
|
|
4
|
+
export type FormInput = {
|
|
5
|
+
/** Its address in the app, `/-/forms/<name>`: lowercase letters, digits, `-` and `_`, unique across the app. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The page's heading; the view's singular name when omitted. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** The fields a visitor fills in, in order: plain fields the `anonymous` role may create. */
|
|
10
|
+
fields: Ref[];
|
|
11
|
+
/** What the page shows once the record is created. */
|
|
12
|
+
thanks: string;
|
|
13
|
+
/** How many submissions one client may send per minute; default 5. */
|
|
14
|
+
perMinute?: number;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { formNamePattern } from "./form-address.js";
|
|
2
|
+
const formModel = (input) => {
|
|
3
|
+
const { name, title, fields, thanks } = input;
|
|
4
|
+
if (!formNamePattern.test(name)) throw new Error(`A form's name goes in its address, so it takes lowercase letters, digits, - and _, not "${name}"`);
|
|
5
|
+
if (fields.length === 0) throw new Error(`Form "${name}" has no fields`);
|
|
6
|
+
const names = fields.map((field) => field.name);
|
|
7
|
+
const repeated = names.find((field, index) => names.indexOf(field) !== index);
|
|
8
|
+
if (repeated !== void 0) throw new Error(`Form "${name}" lists the field "${repeated}" twice`);
|
|
9
|
+
if (thanks.trim() === "") throw new Error(`Form "${name}" needs a thanks text, shown once a visitor sends it`);
|
|
10
|
+
if (title !== void 0 && title.trim() === "") throw new Error(`Form "${name}" has an empty title; leave it out for the view's name`);
|
|
11
|
+
const perMinute = input.perMinute ?? 5;
|
|
12
|
+
if (!Number.isInteger(perMinute) || perMinute < 1) throw new Error(`Form "${name}" takes perMinute as a positive whole number, not ${perMinute}`);
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
...title !== void 0 && { title },
|
|
16
|
+
fields: names,
|
|
17
|
+
thanks,
|
|
18
|
+
perMinute
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { formModel };
|
package/dist/view.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ActionModel, CreateModel, ExportModel, FilterWidgetModel, HelpWhen, Lay
|
|
|
2
2
|
import { FieldRefs, Ref } from "./refs.js";
|
|
3
3
|
import { actions } from "./view-actions.js";
|
|
4
4
|
import { ChartInput } from "./view-chart.js";
|
|
5
|
+
import { FormInput } from "./view-form.js";
|
|
5
6
|
import { ListColumn, columns } from "./view-columns.js";
|
|
6
7
|
import { displays } from "./view-displays.js";
|
|
7
8
|
import { FieldView } from "./view-field.js";
|
|
@@ -68,7 +69,7 @@ export declare class ViewBuilder<R extends {
|
|
|
68
69
|
fields(build: (r: FieldViews<R>) => Record<string, FieldView>): ViewBuilder<R>;
|
|
69
70
|
/**
|
|
70
71
|
* 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
|
+
* columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.calendar`, `d.custom`), one of which
|
|
72
73
|
* may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
|
|
73
74
|
* editable field is not among the columns. See
|
|
74
75
|
* [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
|
|
@@ -97,6 +98,14 @@ export declare class ViewBuilder<R extends {
|
|
|
97
98
|
* 10), counted or with `sum` or `average` that of a numeric field.
|
|
98
99
|
*/
|
|
99
100
|
chart(build: (r: FieldRefs<FieldsOf<R>>) => ChartInput): ViewBuilder<R>;
|
|
101
|
+
/**
|
|
102
|
+
* A public form that creates a record of this resource for anyone, signed in or not, at `/-/forms/<name>`: the
|
|
103
|
+
* `fields` a visitor fills in, the `thanks` shown once it is sent, a `title` (the view's singular name by default) and
|
|
104
|
+
* how many submissions one client may send `perMinute` (default 5). The `anonymous` role must grant `create` of those
|
|
105
|
+
* fields; the form is the only way it creates. Each call adds a form. Throws on a name that does not fit an address, no
|
|
106
|
+
* or repeated fields, or an empty `thanks`. See [Public forms](website/src/content/docs/reference/forms.mdx).
|
|
107
|
+
*/
|
|
108
|
+
form(build: (r: FieldRefs<FieldsOf<R>>) => FormInput): ViewBuilder<R>;
|
|
100
109
|
/**
|
|
101
110
|
* How the record page opens to a user who may change the record: `edit`, the default, as its form; `read` as a read
|
|
102
111
|
* view, its sections as tables of labels and values, with an Edit button to the form; `locked` as the form, locked
|
|
@@ -178,7 +187,7 @@ export declare class ViewBuilder<R extends {
|
|
|
178
187
|
};
|
|
179
188
|
limit?: number;
|
|
180
189
|
}): ViewBuilder<R>;
|
|
181
|
-
/** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf
|
|
190
|
+
/** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf`, the sidebar's `recent` group and the forms' fields. */
|
|
182
191
|
toModel(resource?: ResourceModel): ViewModel;
|
|
183
192
|
private refs;
|
|
184
193
|
private next;
|
package/dist/view.js
CHANGED
|
@@ -3,8 +3,9 @@ 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 { chartModel } from "./view-chart.js";
|
|
6
|
+
import { formModel } from "./view-form.js";
|
|
6
7
|
import { columns, isListColumn } from "./view-columns.js";
|
|
7
|
-
import { boardErrors, displayErrors, displayFields, displays } from "./view-displays.js";
|
|
8
|
+
import { boardErrors, calendarErrors, displayErrors, displayFields, displays } from "./view-displays.js";
|
|
8
9
|
import { FieldView } from "./view-field.js";
|
|
9
10
|
import { widgets } from "./view-widgets.js";
|
|
10
11
|
//#region src/view.ts
|
|
@@ -75,7 +76,7 @@ var ViewBuilder = class ViewBuilder {
|
|
|
75
76
|
}
|
|
76
77
|
/**
|
|
77
78
|
* 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
|
+
* columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.calendar`, `d.custom`), one of which
|
|
79
80
|
* may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
|
|
80
81
|
* editable field is not among the columns. See
|
|
81
82
|
* [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
|
|
@@ -124,6 +125,16 @@ var ViewBuilder = class ViewBuilder {
|
|
|
124
125
|
return this.next({ chart: chartModel(build(this.refs())) });
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
128
|
+
* A public form that creates a record of this resource for anyone, signed in or not, at `/-/forms/<name>`: the
|
|
129
|
+
* `fields` a visitor fills in, the `thanks` shown once it is sent, a `title` (the view's singular name by default) and
|
|
130
|
+
* how many submissions one client may send `perMinute` (default 5). The `anonymous` role must grant `create` of those
|
|
131
|
+
* fields; the form is the only way it creates. Each call adds a form. Throws on a name that does not fit an address, no
|
|
132
|
+
* or repeated fields, or an empty `thanks`. See [Public forms](website/src/content/docs/reference/forms.mdx).
|
|
133
|
+
*/
|
|
134
|
+
form(build) {
|
|
135
|
+
return this.next({ forms: [...this.state.forms ?? [], formModel(build(this.refs()))] });
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
127
138
|
* How the record page opens to a user who may change the record: `edit`, the default, as its form; `read` as a read
|
|
128
139
|
* view, its sections as tables of labels and values, with an Edit button to the form; `locked` as the form, locked
|
|
129
140
|
* until Edit is pressed. A user who may not change it gets the read view in every mode. The form has its own address
|
|
@@ -230,7 +241,7 @@ var ViewBuilder = class ViewBuilder {
|
|
|
230
241
|
};
|
|
231
242
|
return this.next({ searchesOf: searches });
|
|
232
243
|
}
|
|
233
|
-
/** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf
|
|
244
|
+
/** With the resource's model, also verifies the title, search result and list display fields, related column names, a gallery's image, `listOf`, `searchesOf`, the sidebar's `recent` group and the forms' fields. */
|
|
234
245
|
toModel(resource) {
|
|
235
246
|
const { title, nav, searchResult, list, listOf } = this.state;
|
|
236
247
|
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`);
|
|
@@ -244,6 +255,8 @@ var ViewBuilder = class ViewBuilder {
|
|
|
244
255
|
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
256
|
const board = display.kind === "board" ? boardErrors(display, resource) : [];
|
|
246
257
|
if (board.length > 0) throw new Error(`View "${this.state.resource}": display "${display.name}": ${board.join("; ")}`);
|
|
258
|
+
const calendar = display.kind === "calendar" ? calendarErrors(display, resource) : [];
|
|
259
|
+
if (calendar.length > 0) throw new Error(`View "${this.state.resource}": display "${display.name}": ${calendar.join("; ")}`);
|
|
247
260
|
}
|
|
248
261
|
const clash = resource ? list?.relatedColumns?.find((column) => Object.hasOwn(resource.fields, column.name)) : void 0;
|
|
249
262
|
if (clash) throw new Error(`View "${this.state.resource}": related column "${clash.name}" has the name of a field of the resource`);
|
|
@@ -255,6 +268,10 @@ var ViewBuilder = class ViewBuilder {
|
|
|
255
268
|
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`);
|
|
256
269
|
const searchErrors = resource && this.state.searchesOf ? searchesOfErrors(this.state.searchesOf, resource) : [];
|
|
257
270
|
if (searchErrors.length > 0) throw new Error(`View "${this.state.resource}": searchesOf ${searchErrors.join("; ")}`);
|
|
271
|
+
for (const form of resource ? this.state.forms ?? [] : []) {
|
|
272
|
+
const unknown = form.fields.find((name) => !Object.hasOwn(resource.fields, name));
|
|
273
|
+
if (unknown !== void 0) throw new Error(`View "${this.state.resource}": form "${form.name}" names field "${unknown}", which the resource does not have`);
|
|
274
|
+
}
|
|
258
275
|
const recentErrors = resource && nav?.recent ? navRecentErrors(nav.recent, resource) : [];
|
|
259
276
|
if (recentErrors.length > 0) throw new Error(`View "${this.state.resource}": nav.recent ${recentErrors.join("; ")}`);
|
|
260
277
|
return structuredClone(this.state);
|