@protobase/schema 0.12.9 → 0.12.10

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.
@@ -10,7 +10,8 @@ export type AddressCheck = {
10
10
  };
11
11
  /**
12
12
  * What is wrong with a link's address against these models: an unknown resource or page, a `{field}` the record it sits
13
- * on does not have, or a filter or order naming fields the resource does not have. Empty when it can be followed.
13
+ * on does not have, a filter or order naming fields the resource does not have, or create values for fields it does not
14
+ * have. Empty when it can be followed.
14
15
  */
15
16
  export declare const addressErrors: (to: AppAddress, { models, pages, source }: AddressCheck) => string[];
16
17
  //#endregion
@@ -31,7 +31,8 @@ const orderErrors = (model, orderBy) => {
31
31
  };
32
32
  /**
33
33
  * What is wrong with a link's address against these models: an unknown resource or page, a `{field}` the record it sits
34
- * on does not have, or a filter or order naming fields the resource does not have. Empty when it can be followed.
34
+ * on does not have, a filter or order naming fields the resource does not have, or create values for fields it does not
35
+ * have. Empty when it can be followed.
35
36
  */
36
37
  const addressErrors = (to, { models, pages, source }) => {
37
38
  const placeholders = addressPlaceholders(to).flatMap((name) => {
@@ -45,6 +46,7 @@ const addressErrors = (to, { models, pages, source }) => {
45
46
  case "create": {
46
47
  const target = Object.hasOwn(models, to.resource) ? models[to.resource] : void 0;
47
48
  if (!target) return [`unknown resource "${to.resource}"`];
49
+ if (to.kind === "create") return [...placeholders, ...Object.keys(to.values ?? {}).filter((name) => !Object.hasOwn(target.fields, name)).map((name) => `values: unknown field "${name}" on ${to.resource}`)];
48
50
  if (to.kind !== "list") return placeholders;
49
51
  return [
50
52
  ...placeholders,
@@ -4,7 +4,7 @@ import { AppAddress } from "./model.js";
4
4
  export declare const addressPlaceholders: (to: AppAddress) => string[];
5
5
  /** The fields an href takes from the record, from its `{field}` placeholders. */
6
6
  export declare const hrefPlaceholders: (href: string) => string[];
7
- /** `to` with every `{field}` in its key, filter and order replaced by the record's value; a missing value becomes empty. */
7
+ /** `to` with every `{field}` in its key, filter, order or values replaced by the record's value; a missing value becomes empty. */
8
8
  export declare const fillAddress: (to: AppAddress, record?: Record<string, unknown>) => AppAddress;
9
9
  /**
10
10
  * Whether an href leaves the app: it starts with a scheme (`https:`, `mailto:`) or `//`. A path inside the app is an
@@ -4,6 +4,7 @@ const names = (text) => [...text.matchAll(placeholder)].map((match) => match[1])
4
4
  const templated = (to) => {
5
5
  if (to.kind === "record") return (Array.isArray(to.key) ? to.key : [to.key]).filter((part) => typeof part === "string");
6
6
  if (to.kind === "list") return [to.filter, to.orderBy].filter((part) => part !== void 0);
7
+ if (to.kind === "create") return Object.values(to.values ?? {});
7
8
  return [];
8
9
  };
9
10
  /** The fields whose values a link's address takes from the record, from its `{field}` placeholders. */
@@ -11,7 +12,7 @@ const addressPlaceholders = (to) => [...new Set(templated(to).flatMap(names))];
11
12
  /** The fields an href takes from the record, from its `{field}` placeholders. */
12
13
  const hrefPlaceholders = (href) => [...new Set(names(href))];
13
14
  const fill = (text, record) => text.replace(placeholder, (_, name) => record[name] === null || record[name] === void 0 ? "" : String(record[name]));
14
- /** `to` with every `{field}` in its key, filter and order replaced by the record's value; a missing value becomes empty. */
15
+ /** `to` with every `{field}` in its key, filter, order or values replaced by the record's value; a missing value becomes empty. */
15
16
  const fillAddress = (to, record = {}) => {
16
17
  if (to.kind === "record") {
17
18
  const part = (value) => typeof value === "string" ? fill(value, record) : value;
@@ -20,6 +21,10 @@ const fillAddress = (to, record = {}) => {
20
21
  key: Array.isArray(to.key) ? to.key.map(part) : part(to.key)
21
22
  };
22
23
  }
24
+ if (to.kind === "create") return to.values ? {
25
+ ...to,
26
+ values: Object.fromEntries(Object.entries(to.values).map(([name, value]) => [name, fill(value, record)]))
27
+ } : to;
23
28
  if (to.kind !== "list") return to;
24
29
  return {
25
30
  ...to,
@@ -35,7 +35,7 @@ const listQuery = (address) => {
35
35
  const appPath = (address, basePath = "") => {
36
36
  switch (address.kind) {
37
37
  case "page": return `${basePath}/${encodeURIComponent(address.page)}`;
38
- case "create": return `${basePath}/${encodeURIComponent(address.resource)}/${addressWords.create}`;
38
+ case "create": return `${basePath}/${encodeURIComponent(address.resource)}/${addressWords.create}${valuesQuery(address.values)}`;
39
39
  case "record": return `${basePath}/${encodeURIComponent(address.resource)}/${encodeURIComponent(encodeKey(address.key))}`;
40
40
  case "list": return `${basePath}/${encodeURIComponent(address.resource)}${listQuery(address)}`;
41
41
  }
@@ -48,6 +48,10 @@ const decodeSegments = (pathname) => {
48
48
  throw error;
49
49
  }
50
50
  };
51
+ const valuesQuery = (values = {}) => {
52
+ const text = new URLSearchParams(Object.entries(values).filter(([, value]) => value !== "")).toString();
53
+ return text ? `?${text}` : "";
54
+ };
51
55
  const filterLayout = (value) => value === "bar" || value === "panel" ? value : void 0;
52
56
  /**
53
57
  * The address `pathAndQuery` (below the base path) shows, or `undefined` for anything else: the root, the app's own
@@ -63,10 +67,14 @@ const readAppPath = (pathAndQuery, pages) => {
63
67
  kind: "page",
64
68
  page: first
65
69
  } : void 0;
66
- if (second === addressWords.create) return {
67
- kind: "create",
68
- resource: first
69
- };
70
+ if (second === addressWords.create) {
71
+ const values = Object.fromEntries([...url.searchParams].filter(([, value]) => value !== ""));
72
+ return {
73
+ kind: "create",
74
+ resource: first,
75
+ ...Object.keys(values).length > 0 && { values }
76
+ };
77
+ }
70
78
  if (second !== void 0) return {
71
79
  kind: "record",
72
80
  resource: first,
package/dist/model.d.ts CHANGED
@@ -87,7 +87,8 @@ export type ListQuery = {
87
87
  export type FilterLayout = 'bar' | 'panel';
88
88
  /**
89
89
  * 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
90
- * its one value or its parts, as the record API takes them; `readAppPath` gives it back as the API's text form.
90
+ * its one value or its parts, as the record API takes them; `readAppPath` gives it back as the API's text form. A create
91
+ * form's `values` fill in fields of the new record by name, as text (`{ listing_id: '42' }`).
91
92
  */
92
93
  export type AppAddress = {
93
94
  kind: 'list';
@@ -103,6 +104,7 @@ export type AppAddress = {
103
104
  } | {
104
105
  kind: 'create';
105
106
  resource: string;
107
+ values?: Readonly<Record<string, string>>;
106
108
  } | {
107
109
  kind: 'page';
108
110
  page: string;
@@ -136,6 +138,7 @@ export type ListDisplayModel = {
136
138
  name: string;
137
139
  label?: string;
138
140
  icon?: string;
141
+ image?: string;
139
142
  title?: string;
140
143
  subtitle?: string;
141
144
  badge?: string;
@@ -17,12 +17,14 @@ type Naming = {
17
17
  export declare const tableDisplay = "table";
18
18
  export declare const displays: {
19
19
  /**
20
- * A card per record: `title` (default: the view's title) over `subtitle`, `badge` drawn as a badge with its value
21
- * labels, and a few small `meta` fields such as dates. With `card`, a component of the project or a plugin draws each
20
+ * A card per record: `image`, a file field shown as a picture across the card's top, then `title` (default: the
21
+ * view's title) over `subtitle`, `badge` drawn as a badge with its value labels, and a few small `meta` fields such as
22
+ * dates. With `card`, a component of the project or a plugin draws each
22
23
  * card's content instead, from the row of those fields (`{ row }`); without it, or when it throws, the card is the
23
24
  * built-in one. Named `cards` unless `name` says otherwise.
24
25
  */
25
26
  cards: (fields: {
27
+ image?: Ref;
26
28
  title?: Ref;
27
29
  subtitle?: Ref;
28
30
  badge?: Ref;
@@ -13,8 +13,9 @@ const customComponent = (component, where) => {
13
13
  };
14
14
  const displays = {
15
15
  /**
16
- * A card per record: `title` (default: the view's title) over `subtitle`, `badge` drawn as a badge with its value
17
- * labels, and a few small `meta` fields such as dates. With `card`, a component of the project or a plugin draws each
16
+ * A card per record: `image`, a file field shown as a picture across the card's top, then `title` (default: the
17
+ * view's title) over `subtitle`, `badge` drawn as a badge with its value labels, and a few small `meta` fields such as
18
+ * dates. With `card`, a component of the project or a plugin draws each
18
19
  * card's content instead, from the row of those fields (`{ row }`); without it, or when it throws, the card is the
19
20
  * built-in one. Named `cards` unless `name` says otherwise.
20
21
  */
@@ -22,6 +23,7 @@ const displays = {
22
23
  kind: "cards",
23
24
  name: o.name ?? "cards",
24
25
  ...naming(o),
26
+ ...fields.image && { image: fields.image.name },
25
27
  ...fields.title && { title: fields.title.name },
26
28
  ...fields.subtitle && { subtitle: fields.subtitle.name },
27
29
  ...fields.badge && { badge: fields.badge.name },
@@ -55,6 +57,7 @@ const displayErrors = (list) => {
55
57
  };
56
58
  /** Every field a display names: what it shows, and the field references in a custom display's props. */
57
59
  const displayFields = (display) => display.kind === "cards" ? [
60
+ display.image,
58
61
  display.title,
59
62
  display.subtitle,
60
63
  display.badge,
package/dist/view.js CHANGED
@@ -97,6 +97,7 @@ var ViewBuilder = class ViewBuilder {
97
97
  for (const display of resource ? list?.displays ?? [] : []) {
98
98
  const unknown = displayFields(display).find((name) => !Object.hasOwn(resource.fields, name));
99
99
  if (unknown !== void 0) throw new Error(`View "${this.state.resource}": display "${display.name}" names field "${unknown}", which the resource does not have`);
100
+ if (display.kind === "cards" && display.image !== void 0 && resource.fields[display.image].type !== "file") throw new Error(`View "${this.state.resource}": display "${display.name}" shows "${display.image}" as its image, which is not a file field`);
100
101
  }
101
102
  const recentErrors = resource && nav?.recent ? navRecentErrors(nav.recent, resource) : [];
102
103
  if (recentErrors.length > 0) throw new Error(`View "${this.state.resource}": nav.recent ${recentErrors.join("; ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.12.9",
3
+ "version": "0.12.10",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {