@protobase/layout 0.19.16 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -102,6 +102,23 @@ const blockSpecs = {
102
102
  span: optional("span")
103
103
  }
104
104
  },
105
+ Chart: {
106
+ children: false,
107
+ props: {
108
+ ...heading,
109
+ resource: required("text"),
110
+ filter: optional("text"),
111
+ groupBy: required("text"),
112
+ type: optional("chartType"),
113
+ sum: optional("text"),
114
+ average: optional("text"),
115
+ granularity: optional("bucket"),
116
+ range: optional("range"),
117
+ limit: optional("integer"),
118
+ empty: optional("text"),
119
+ span: optional("span")
120
+ }
121
+ },
105
122
  Progress: {
106
123
  children: false,
107
124
  props: {
@@ -159,6 +176,26 @@ const variants = [
159
176
  "danger",
160
177
  "ghost"
161
178
  ];
179
+ const chartTypes = [
180
+ "bar",
181
+ "line",
182
+ "donut"
183
+ ];
184
+ const buckets = [
185
+ "hour",
186
+ "day",
187
+ "week",
188
+ "month",
189
+ "year"
190
+ ];
191
+ const ranges = [
192
+ "7d",
193
+ "30d",
194
+ "90d",
195
+ "1y",
196
+ "all"
197
+ ];
198
+ const oneOf = (values) => [(value) => typeof value === "string" && values.includes(value), values.join(", ")];
162
199
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
163
200
  const checks = {
164
201
  text: [(value) => typeof value === "string", "text"],
@@ -174,6 +211,9 @@ const checks = {
174
211
  label: [(value) => typeof value === "string" || value === false, "text, or false for none"],
175
212
  amount: [(value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value), "a number or a field name"],
176
213
  scalar: [(value) => typeof value === "string" || typeof value === "number", "text or a number"],
214
+ chartType: oneOf(chartTypes),
215
+ bucket: oneOf(buckets),
216
+ range: oneOf(ranges),
177
217
  address: [(value) => isObject(value) && !isLayoutNode(value) && typeof value.kind === "string", "an address, such as { kind: 'list', resource: 'orders' }"]
178
218
  };
179
219
  /** Problems with one element's props and children against its block's spec. */
package/dist/blocks.d.ts CHANGED
@@ -83,6 +83,34 @@ export type RecordRowProps = DataProps & Heading & {
83
83
  empty?: string;
84
84
  span?: GridSpan;
85
85
  };
86
+ /** How a chart draws its groups: bars, a line through a date field's buckets, or a donut of a field's values. */
87
+ export type ChartType = 'bar' | 'line' | 'donut';
88
+ /** How long each bucket of a chart over a date or timestamp field is. */
89
+ export type ChartBucket = 'hour' | 'day' | 'week' | 'month' | 'year';
90
+ /**
91
+ * Records of `resource` matching `filter`, grouped and added up as a chart: how many have each value of `groupBy`, or
92
+ * fall in each bucket of it when it is a date or timestamp field, or with `sum` or `average` that of a numeric field.
93
+ */
94
+ export type ChartProps = Heading & {
95
+ resource: string;
96
+ filter?: string;
97
+ /** A filterable field, such as an enum, a boolean or a relation; an index must serve it. */
98
+ groupBy: string;
99
+ /** Default `bar`; `line` is for a date `groupBy`, `donut` for one of values. */
100
+ type?: ChartType;
101
+ /** A numeric field each group adds up, instead of counting its records. */
102
+ sum?: string;
103
+ /** A numeric field each group averages, instead of counting its records. */
104
+ average?: string;
105
+ /** A date `groupBy`'s bucket; default `day`. */
106
+ granularity?: ChartBucket;
107
+ /** How far back a date `groupBy` reaches; default `30d`. */
108
+ range?: '7d' | '30d' | '90d' | '1y' | 'all';
109
+ /** How many values a chart of values shows, the largest; default 10. */
110
+ limit?: number;
111
+ empty?: string;
112
+ span?: GridSpan;
113
+ };
86
114
  export type ProgressProps = {
87
115
  /** A number, or a field of the record around it. */
88
116
  value: number | string;
@@ -137,6 +165,7 @@ export declare const Field: Block<FieldProps>;
137
165
  export declare const Table: Block<TableProps>;
138
166
  export declare const CardRow: Block<CardRowProps>;
139
167
  export declare const RecordRow: Block<RecordRowProps>;
168
+ export declare const Chart: Block<ChartProps>;
140
169
  export declare const Progress: Block<ProgressProps>;
141
170
  export declare const ModalForm: Block<ModalFormProps>;
142
171
  export declare const Action: Block<ActionProps>;
package/dist/blocks.js CHANGED
@@ -9,10 +9,11 @@ const Field = defineBlock("Field", false);
9
9
  const Table = defineBlock("Table", false);
10
10
  const CardRow = defineBlock("CardRow", false);
11
11
  const RecordRow = defineBlock("RecordRow", false);
12
+ const Chart = defineBlock("Chart", false);
12
13
  const Progress = defineBlock("Progress", false);
13
14
  const ModalForm = defineBlock("ModalForm", false);
14
15
  const Action = defineBlock("Action", false);
15
16
  const Show = defineBlock("Show", false);
16
17
  const Link = defineBlock("Link", false);
17
18
  //#endregion
18
- export { Action, Card, CardRow, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordRow, Show, Stat, Table };
19
+ export { Action, Card, CardRow, Chart, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordRow, Show, Stat, Table };
@@ -0,0 +1,51 @@
1
+ //#region src/check-chart.ts
2
+ const numericTypes = [
3
+ "integer",
4
+ "bigint",
5
+ "decimal"
6
+ ];
7
+ const ungroupable = ["json", "file"];
8
+ const text = (value) => typeof value === "string" ? value : void 0;
9
+ /** Whether a chart's `groupBy` is a date or timestamp field, which it buckets, rather than a field of values. */
10
+ const isDatedGroup = (field) => field?.type === "date" || field?.type === "timestamp";
11
+ /** The fields a chart reads: the one it groups by, and the one it adds up when it has `sum` or `average`. */
12
+ const chartFields = (node) => [text(node.props.groupBy), text(node.props.sum) ?? text(node.props.average)].filter((name) => name !== void 0);
13
+ const fieldOf = (model, name) => Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
14
+ const groupProblems = (model, name) => {
15
+ const field = fieldOf(model, name);
16
+ if (!field) return [`groupBy: unknown field "${name}" on ${model.name}`];
17
+ if (field.sensitive) return [`groupBy: "${name}" is sensitive, so a chart cannot show its values`];
18
+ if (ungroupable.includes(field.type)) return [`groupBy: "${name}" is a ${field.type} field, which has no values to group by`];
19
+ return field.filterable ? [] : [`groupBy: "${name}" is not filterable, so the API refuses to group by it: mark "${name}" .filterable()`];
20
+ };
21
+ const measureProblems = (model, prop, name) => {
22
+ const field = fieldOf(model, name);
23
+ if (!field) return [`${prop}: unknown field "${name}" on ${model.name}`];
24
+ if (field.sensitive) return [`${prop}: "${name}" is sensitive, so a chart cannot add it up`];
25
+ return numericTypes.includes(field.type) ? [] : [`${prop}: "${name}" is ${field.type}, and only a number can be added up`];
26
+ };
27
+ const shapeProblems = (node, dated) => {
28
+ const type = text(node.props.type) ?? "bar";
29
+ const given = (names) => names.filter((name) => node.props[name] !== void 0);
30
+ if (dated) return [...type === "donut" ? ["a donut shares out values, so it needs a groupBy of values, not dates"] : [], ...given(["limit"]).map((name) => `"${name}" is for a groupBy of values, not dates`)];
31
+ return [
32
+ ...type === "line" ? ["a line runs through time, so it needs a date or timestamp groupBy"] : [],
33
+ ...type === "donut" && node.props.average !== void 0 ? ["a donut shares out a total, and averages add up to none: count or sum instead"] : [],
34
+ ...given(["granularity", "range"]).map((name) => `"${name}" is for a date or timestamp groupBy`)
35
+ ];
36
+ };
37
+ /** What a chart names that its resource does not allow: the field it groups by, the one it adds up, and its shape. */
38
+ const chartProblems = (node, model) => {
39
+ const groupBy = String(node.props.groupBy);
40
+ const sum = text(node.props.sum);
41
+ const average = text(node.props.average);
42
+ const grouped = groupProblems(model, groupBy);
43
+ return [
44
+ ...grouped,
45
+ ...sum !== void 0 ? measureProblems(model, "sum", sum) : [],
46
+ ...average !== void 0 ? measureProblems(model, "average", average) : [],
47
+ ...grouped.length === 0 ? shapeProblems(node, isDatedGroup(fieldOf(model, groupBy))) : []
48
+ ];
49
+ };
50
+ //#endregion
51
+ export { chartFields, chartProblems, isDatedGroup };
@@ -1,6 +1,7 @@
1
1
  import { dataProps } from "./node.js";
2
2
  import { countedResource, parseCondition } from "./condition.js";
3
3
  import { walkTree } from "./walk.js";
4
+ import { chartProblems } from "./check-chart.js";
4
5
  import { resolveFieldPath } from "./field-path.js";
5
6
  import { checkFilter, checkOrderBy, parseFilter, parseOrderBy } from "@protobase/schema";
6
7
  import { addressErrors, fieldRefsIn } from "@protobase/schema/internal";
@@ -49,6 +50,9 @@ const nodeProblems = (node, scope, { models, actions, pages }) => {
49
50
  case "Stat":
50
51
  if (text(node.props.field) !== void 0) path(String(node.props.field));
51
52
  break;
53
+ case "Chart":
54
+ if (model) problems.push(...chartProblems(node, model));
55
+ break;
52
56
  case "Progress":
53
57
  for (const name of [node.props.value, node.props.max]) if (typeof name === "string") path(name);
54
58
  break;
@@ -25,6 +25,7 @@ const statProblems = ({ props }) => {
25
25
  ].filter((name) => props[name] !== void 0).length !== 1) return ["takes exactly one of \"value\", \"resource\" (a count) and \"field\""];
26
26
  return props.filter !== void 0 && props.resource === void 0 ? ["\"filter\" needs \"resource\""] : [];
27
27
  };
28
+ const chartStructureProblems = ({ props }) => props.sum !== void 0 && props.average !== void 0 ? ["takes \"sum\" or \"average\", not both"] : [];
28
29
  const linkProblems = ({ props }) => {
29
30
  if (props.to === void 0 === (props.href === void 0)) return ["takes exactly one of \"to\" (an address in the app) and \"href\" (a link that leaves it)"];
30
31
  const error = typeof props.href === "string" ? appHrefError(props.href) : void 0;
@@ -47,6 +48,7 @@ const structureProblems = (tree) => {
47
48
  propProblems(spec, node.props, node.children.length).forEach(report);
48
49
  recordProblems(node, scope).forEach(report);
49
50
  if (node.type === "Stat") statProblems(node).forEach(report);
51
+ if (node.type === "Chart") chartStructureProblems(node).forEach(report);
50
52
  if (node.type === "Link") linkProblems(node).forEach(report);
51
53
  if (node.type === "Show" && typeof node.props.when === "string") {
52
54
  const parsed = parseCondition(node.props.when);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Block, LayoutChild, LayoutChildren, LayoutNode, LayoutValue, isLayoutNode } from "./node.js";
2
- import { Action, ActionProps, ButtonVariant, Card, CardProps, CardRow, CardRowProps, DataProps, Elements, Field, FieldProps, Grid, GridProps, GridSpan, Link, LinkProps, ModalForm, ModalFormProps, Page, PageProps, Progress, ProgressProps, RecordCard, RecordCardProps, RecordRow, RecordRowProps, Show, ShowProps, Stat, StatProps, Table, TableProps } from "./blocks.js";
2
+ import { Action, ActionProps, ButtonVariant, Card, CardProps, CardRow, CardRowProps, Chart, ChartBucket, ChartProps, ChartType, DataProps, Elements, Field, FieldProps, Grid, GridProps, GridSpan, Link, LinkProps, ModalForm, ModalFormProps, Page, PageProps, Progress, ProgressProps, RecordCard, RecordCardProps, RecordRow, RecordRowProps, Show, ShowProps, Stat, StatProps, Table, TableProps } from "./blocks.js";
3
3
  import { ComponentOptions, component } from "./component.js";
4
4
  import { PageDefinition, PageModel, PageOptions, isPageDefinition, page } from "./page.js";
5
5
  import { Location, walkTree } from "./walk.js";
@@ -9,4 +9,4 @@ import { countedResource, evaluateCondition, parseCondition } from "./condition.
9
9
  import { PathStep, resolveFieldPath } from "./field-path.js";
10
10
  import { jsx_runtime_d_exports } from "./jsx-runtime.js";
11
11
  import { FieldRefValue, fieldRef } from "@protobase/schema";
12
- export { Action, type ActionProps, type Block, type ButtonVariant, Card, type CardProps, CardRow, type CardRowProps, type ComponentOptions, type DataProps, type Elements, Field, type FieldProps, type FieldRefValue, Grid, type GridProps, type GridSpan, type LayoutChild, type LayoutChildren, type LayoutNode, type LayoutValue, Link, type LinkProps, type Location, ModalForm, type ModalFormProps, Page, type PageDefinition, type PageModel, type PageOptions, type PageProps, type PathStep, Progress, type ProgressProps, RecordCard, type RecordCardProps, RecordRow, type RecordRowProps, Show, type ShowProps, Stat, type StatProps, Table, type TableProps, checkPages, component, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_d_exports as jsxRuntime, page, parseCondition, prunePage, resolveFieldPath, walkTree };
12
+ export { Action, type ActionProps, type Block, type ButtonVariant, Card, type CardProps, CardRow, type CardRowProps, Chart, type ChartBucket, type ChartProps, type ChartType, type ComponentOptions, type DataProps, type Elements, Field, type FieldProps, type FieldRefValue, Grid, type GridProps, type GridSpan, type LayoutChild, type LayoutChildren, type LayoutNode, type LayoutValue, Link, type LinkProps, type Location, ModalForm, type ModalFormProps, Page, type PageDefinition, type PageModel, type PageOptions, type PageProps, type PathStep, Progress, type ProgressProps, RecordCard, type RecordCardProps, RecordRow, type RecordRowProps, Show, type ShowProps, Stat, type StatProps, Table, type TableProps, checkPages, component, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_d_exports as jsxRuntime, page, parseCondition, prunePage, resolveFieldPath, walkTree };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isLayoutNode } from "./node.js";
2
- import { Action, Card, CardRow, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordRow, Show, Stat, Table } from "./blocks.js";
2
+ import { Action, Card, CardRow, Chart, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordRow, Show, Stat, Table } from "./blocks.js";
3
3
  import { component } from "./component.js";
4
4
  import { countedResource, evaluateCondition, parseCondition } from "./condition.js";
5
5
  import { walkTree } from "./walk.js";
@@ -9,4 +9,4 @@ import { checkPages } from "./check-page.js";
9
9
  import { prunePage } from "./prune-page.js";
10
10
  import { jsx_runtime_exports } from "./jsx-runtime.js";
11
11
  import { fieldRef } from "@protobase/schema";
12
- export { Action, Card, CardRow, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordRow, Show, Stat, Table, checkPages, component, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_exports as jsxRuntime, page, parseCondition, prunePage, resolveFieldPath, walkTree };
12
+ export { Action, Card, CardRow, Chart, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordRow, Show, Stat, Table, checkPages, component, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_exports as jsxRuntime, page, parseCondition, prunePage, resolveFieldPath, walkTree };
@@ -1,6 +1,7 @@
1
1
  import { dataProps } from "./node.js";
2
2
  import { countedResource, parseCondition } from "./condition.js";
3
3
  import { mapTree } from "./walk.js";
4
+ import { chartFields } from "./check-chart.js";
4
5
  import { resolveFieldPath } from "./field-path.js";
5
6
  import { elementResource, linkSource } from "./check-page.js";
6
7
  import { checkFilter, checkOrderBy, parseFilter, parseOrderBy } from "@protobase/schema";
@@ -37,6 +38,7 @@ const prune = (node, scope, access) => {
37
38
  switch (node.type) {
38
39
  case "Field": return readable(String(node.props.name)) ? node : null;
39
40
  case "Stat": return text(node.props.field) === void 0 || readable(String(node.props.field)) ? node : null;
41
+ case "Chart": return model && chartFields(node).every((name) => Object.hasOwn(model.fields, name)) ? node : null;
40
42
  case "Progress": return [node.props.value, node.props.max].every((name) => typeof name !== "string" || readable(name)) ? node : null;
41
43
  case "Table": {
42
44
  const columns = node.props.columns;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/layout",
3
- "version": "0.19.16",
3
+ "version": "0.20.0",
4
4
  "description": "Composed pages for Protobase, written as JSX that compiles to plain data, with its JSX runtime.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@protobase/schema": "0.19.16",
34
+ "@protobase/schema": "0.20.0",
35
35
  "aip-parsers": "^1.0.0"
36
36
  }
37
37
  }