@protobase/layout 0.19.15 → 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.
- package/dist/block-specs.js +40 -0
- package/dist/blocks.d.ts +29 -0
- package/dist/blocks.js +2 -1
- package/dist/check-chart.js +51 -0
- package/dist/check-page.js +4 -0
- package/dist/check-structure.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/prune-page.js +2 -0
- package/package.json +2 -2
package/dist/block-specs.js
CHANGED
|
@@ -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 };
|
package/dist/check-page.js
CHANGED
|
@@ -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;
|
package/dist/check-structure.js
CHANGED
|
@@ -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 };
|
package/dist/prune-page.js
CHANGED
|
@@ -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.
|
|
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.
|
|
34
|
+
"@protobase/schema": "0.20.0",
|
|
35
35
|
"aip-parsers": "^1.0.0"
|
|
36
36
|
}
|
|
37
37
|
}
|