@appsemble/lang-sdk 0.37.6 → 0.38.1
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/gridUtils.d.ts +63 -0
- package/gridUtils.js +114 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/iterApp.js +7 -2
- package/package.json +2 -2
- package/pageUtils.d.ts +9 -0
- package/pageUtils.js +41 -0
- package/schemas/AppLayoutDefinition.js +23 -0
- package/schemas/BasePageDefinition.js +19 -0
- package/schemas/BlockDefinition.js +4 -1
- package/schemas/GridBreakpointsDefinition.d.ts +2 -0
- package/schemas/GridBreakpointsDefinition.js +22 -0
- package/schemas/GridSpacingDefinition.js +2 -2
- package/schemas/LoopPageDefinition.js +8 -0
- package/schemas/NavbarDeviceGridLayoutDefinition.d.ts +1 -0
- package/schemas/NavbarDeviceGridLayoutDefinition.js +9 -0
- package/schemas/NavbarLayoutDefinition.d.ts +1 -0
- package/schemas/NavbarLayoutDefinition.js +24 -0
- package/schemas/PageLayoutDefinition.d.ts +1 -2
- package/schemas/PageLayoutDefinition.js +10 -14
- package/schemas/PageParentDefinition.d.ts +2 -0
- package/schemas/PageParentDefinition.js +35 -0
- package/schemas/ResourceDefinition.js +1 -1
- package/schemas/ResponsiveGridLayoutDefinition.d.ts +2 -0
- package/schemas/ResponsiveGridLayoutDefinition.js +18 -0
- package/schemas/index.d.ts +5 -0
- package/schemas/index.js +5 -0
- package/types/core.d.ts +46 -1
- package/types/gridLayout.d.ts +20 -6
- package/validation.js +210 -42
package/gridUtils.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type GridLayoutDefinition, type PageDefinition, type PageLayoutDefinition, type ResponsiveGridLayoutDefinition } from './types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The template area a page grid layout uses to place the breadcrumb trail.
|
|
4
|
+
*/
|
|
5
|
+
export declare const breadcrumbsGridArea = "breadcrumbs";
|
|
6
|
+
/**
|
|
7
|
+
* The devices a responsive grid layout is defined for, from the smallest breakpoint up.
|
|
8
|
+
*/
|
|
9
|
+
export declare const gridDeviceOrder: readonly ["mobile", "tablet", "desktop"];
|
|
10
|
+
/**
|
|
11
|
+
* One of the devices a responsive grid layout is defined for.
|
|
12
|
+
*/
|
|
13
|
+
export type GridDeviceName = (typeof gridDeviceOrder)[number];
|
|
14
|
+
/**
|
|
15
|
+
* The grid a device falls back to when neither it nor a smaller device defines one.
|
|
16
|
+
*/
|
|
17
|
+
export declare const defaultGridLayout: Readonly<GridLayoutDefinition>;
|
|
18
|
+
/**
|
|
19
|
+
* Collect the template areas a responsive grid layout defines.
|
|
20
|
+
*
|
|
21
|
+
* @param layoutDefinition The responsive grid layout to read.
|
|
22
|
+
* @returns Every area named by the template of any device layout, without the `.` placeholder.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getGridTemplateAreas(layoutDefinition: ResponsiveGridLayoutDefinition | undefined): Set<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Collect the grid layouts of a page, in definition order.
|
|
27
|
+
*
|
|
28
|
+
* A page type that splits its content over sub pages defines a grid per sub page, so a tab, a step
|
|
29
|
+
* or a loop sub page each carry their own. A basic page has a single grid, and a container page has
|
|
30
|
+
* none of its own.
|
|
31
|
+
*
|
|
32
|
+
* @param page The page to read the grid layouts of.
|
|
33
|
+
* @returns The layout of every sub page the page renders, which is undefined where none is defined.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPageGridLayouts(page: PageDefinition): (PageLayoutDefinition | undefined)[];
|
|
36
|
+
/**
|
|
37
|
+
* Collect the template areas each device renders.
|
|
38
|
+
*
|
|
39
|
+
* A device that defines no template of its own inherits the one of the next smaller device, the way
|
|
40
|
+
* the app applies the grid of a page, down to a fallback of the single `main` area. So the areas a
|
|
41
|
+
* device renders are not the areas its own template names.
|
|
42
|
+
*
|
|
43
|
+
* @param layout The responsive grid layout to read.
|
|
44
|
+
* @returns The areas rendered by each device, keyed by device name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getCascadedGridTemplateAreas(layout: ResponsiveGridLayoutDefinition | undefined): Record<GridDeviceName, Set<string>>;
|
|
47
|
+
/**
|
|
48
|
+
* Check whether a grid layout places the breadcrumb trail.
|
|
49
|
+
*
|
|
50
|
+
* Every device has to render the area, since a device that does not would place the trail against
|
|
51
|
+
* implicit grid lines rather than inside the layout.
|
|
52
|
+
*
|
|
53
|
+
* @param layout The responsive grid layout to check.
|
|
54
|
+
* @returns Whether every device renders the breadcrumbs template area.
|
|
55
|
+
*/
|
|
56
|
+
export declare function hasBreadcrumbsGridArea(layout: PageLayoutDefinition | undefined): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Check whether a page renders the breadcrumb trail from a grid layout.
|
|
59
|
+
*
|
|
60
|
+
* @param page The page to check the grid layouts of.
|
|
61
|
+
* @returns Whether any grid layout of the page places the breadcrumb trail.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pageHasBreadcrumbsGridArea(page: PageDefinition): boolean;
|
package/gridUtils.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The template area a page grid layout uses to place the breadcrumb trail.
|
|
3
|
+
*/
|
|
4
|
+
export const breadcrumbsGridArea = 'breadcrumbs';
|
|
5
|
+
/**
|
|
6
|
+
* The devices a responsive grid layout is defined for, from the smallest breakpoint up.
|
|
7
|
+
*/
|
|
8
|
+
export const gridDeviceOrder = ['mobile', 'tablet', 'desktop'];
|
|
9
|
+
/**
|
|
10
|
+
* The grid a device falls back to when neither it nor a smaller device defines one.
|
|
11
|
+
*/
|
|
12
|
+
export const defaultGridLayout = {
|
|
13
|
+
columns: 1,
|
|
14
|
+
template: ['main'],
|
|
15
|
+
};
|
|
16
|
+
function getTemplateAreas(template) {
|
|
17
|
+
const areas = new Set();
|
|
18
|
+
for (const row of template) {
|
|
19
|
+
for (const area of row.split(' ')) {
|
|
20
|
+
if (area && area !== '.') {
|
|
21
|
+
areas.add(area);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return areas;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Collect the template areas a responsive grid layout defines.
|
|
29
|
+
*
|
|
30
|
+
* @param layoutDefinition The responsive grid layout to read.
|
|
31
|
+
* @returns Every area named by the template of any device layout, without the `.` placeholder.
|
|
32
|
+
*/
|
|
33
|
+
export function getGridTemplateAreas(layoutDefinition) {
|
|
34
|
+
const areas = new Set();
|
|
35
|
+
if (!layoutDefinition) {
|
|
36
|
+
return areas;
|
|
37
|
+
}
|
|
38
|
+
for (const deviceDefinition of Object.values(layoutDefinition)) {
|
|
39
|
+
for (const area of getTemplateAreas(deviceDefinition?.layout?.template ?? [])) {
|
|
40
|
+
areas.add(area);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return areas;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Collect the grid layouts of a page, in definition order.
|
|
47
|
+
*
|
|
48
|
+
* A page type that splits its content over sub pages defines a grid per sub page, so a tab, a step
|
|
49
|
+
* or a loop sub page each carry their own. A basic page has a single grid, and a container page has
|
|
50
|
+
* none of its own.
|
|
51
|
+
*
|
|
52
|
+
* @param page The page to read the grid layouts of.
|
|
53
|
+
* @returns The layout of every sub page the page renders, which is undefined where none is defined.
|
|
54
|
+
*/
|
|
55
|
+
export function getPageGridLayouts(page) {
|
|
56
|
+
switch (page.type) {
|
|
57
|
+
case 'container':
|
|
58
|
+
return [];
|
|
59
|
+
case 'flow':
|
|
60
|
+
return page.steps.map((step) => step.layout);
|
|
61
|
+
case 'loop':
|
|
62
|
+
return [page.start, page.foreach, page.end].flatMap((subPage) => subPage ? [subPage.layout] : []);
|
|
63
|
+
case 'tabs':
|
|
64
|
+
return page.tabs
|
|
65
|
+
? page.tabs.map((tab) => tab.layout)
|
|
66
|
+
: page.definition
|
|
67
|
+
? [page.definition.foreach.layout]
|
|
68
|
+
: [];
|
|
69
|
+
default:
|
|
70
|
+
return [page.layout];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Collect the template areas each device renders.
|
|
75
|
+
*
|
|
76
|
+
* A device that defines no template of its own inherits the one of the next smaller device, the way
|
|
77
|
+
* the app applies the grid of a page, down to a fallback of the single `main` area. So the areas a
|
|
78
|
+
* device renders are not the areas its own template names.
|
|
79
|
+
*
|
|
80
|
+
* @param layout The responsive grid layout to read.
|
|
81
|
+
* @returns The areas rendered by each device, keyed by device name.
|
|
82
|
+
*/
|
|
83
|
+
export function getCascadedGridTemplateAreas(layout) {
|
|
84
|
+
let { template } = defaultGridLayout;
|
|
85
|
+
return Object.fromEntries(gridDeviceOrder.map((device) => {
|
|
86
|
+
template = layout?.[device]?.layout?.template ?? template;
|
|
87
|
+
return [device, getTemplateAreas(template)];
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check whether a grid layout places the breadcrumb trail.
|
|
92
|
+
*
|
|
93
|
+
* Every device has to render the area, since a device that does not would place the trail against
|
|
94
|
+
* implicit grid lines rather than inside the layout.
|
|
95
|
+
*
|
|
96
|
+
* @param layout The responsive grid layout to check.
|
|
97
|
+
* @returns Whether every device renders the breadcrumbs template area.
|
|
98
|
+
*/
|
|
99
|
+
export function hasBreadcrumbsGridArea(layout) {
|
|
100
|
+
if (!layout) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return Object.values(getCascadedGridTemplateAreas(layout)).every((areas) => areas.has(breadcrumbsGridArea));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check whether a page renders the breadcrumb trail from a grid layout.
|
|
107
|
+
*
|
|
108
|
+
* @param page The page to check the grid layouts of.
|
|
109
|
+
* @returns Whether any grid layout of the page places the breadcrumb trail.
|
|
110
|
+
*/
|
|
111
|
+
export function pageHasBreadcrumbsGridArea(page) {
|
|
112
|
+
return getPageGridLayouts(page).some(hasBreadcrumbsGridArea);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=gridUtils.js.map
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './authorization.js';
|
|
|
6
6
|
export * from './blockUtils.js';
|
|
7
7
|
export * from './validation.js';
|
|
8
8
|
export * from './findPageByName.js';
|
|
9
|
+
export * from './gridUtils.js';
|
|
9
10
|
export * from './pageUtils.js';
|
|
10
11
|
export * from './iterApp.js';
|
|
11
12
|
export * from './serverActions.js';
|
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './authorization.js';
|
|
|
6
6
|
export * from './blockUtils.js';
|
|
7
7
|
export * from './validation.js';
|
|
8
8
|
export * from './findPageByName.js';
|
|
9
|
+
export * from './gridUtils.js';
|
|
9
10
|
export * from './pageUtils.js';
|
|
10
11
|
export * from './iterApp.js';
|
|
11
12
|
export * from './serverActions.js';
|
package/iterApp.js
CHANGED
|
@@ -121,8 +121,13 @@ export function iterPage(page, callbacks, prefix = []) {
|
|
|
121
121
|
if (result) {
|
|
122
122
|
return true;
|
|
123
123
|
}
|
|
124
|
-
const { foreach } = page;
|
|
125
|
-
|
|
124
|
+
const { end, foreach, start } = page;
|
|
125
|
+
const subPages = [
|
|
126
|
+
['steps.first', start ?? foreach],
|
|
127
|
+
['steps', foreach],
|
|
128
|
+
['steps.last', end ?? foreach],
|
|
129
|
+
];
|
|
130
|
+
return subPages.some(([suffix, subPage]) => iterBlockList(subPage?.blocks ?? [], callbacks, [...prefix, suffix, 'blocks']));
|
|
126
131
|
}
|
|
127
132
|
if (page.type === 'container') {
|
|
128
133
|
let result = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/lang-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"description": "Language SDK for Appsemble",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/language-tags": "1.0.4",
|
|
60
60
|
"@types/lcm": "0.0.2",
|
|
61
|
-
"vitest": "4.1.
|
|
61
|
+
"vitest": "4.1.11"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=24"
|
package/pageUtils.d.ts
CHANGED
|
@@ -5,4 +5,13 @@ export declare function getPageMessageId(pageName: string): string;
|
|
|
5
5
|
export declare function getPagePathSegment(page: string | NamedPage): string;
|
|
6
6
|
export declare function getPageDisplayName(page: NamedPage, getAppMessage: MessageGetter): string;
|
|
7
7
|
export declare function findPageById(pages: PageDefinition[], normalizedPageId: string, appMessageIds: string[], getAppMessage: MessageGetter): PageDefinition | null;
|
|
8
|
+
/**
|
|
9
|
+
* Collect the pages a page sits under, by following `parent` upwards.
|
|
10
|
+
*
|
|
11
|
+
* @param pages The pages of the app.
|
|
12
|
+
* @param page The page to collect the ancestors of.
|
|
13
|
+
* @param appMemberRoles The roles held by the app member, used to resolve role-scoped parents.
|
|
14
|
+
* @returns The ancestors, root first, excluding the page itself.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getPageAncestors(pages: PageDefinition[], page: PageDefinition, appMemberRoles?: string[]): PageDefinition[];
|
|
8
17
|
export {};
|
package/pageUtils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findPageByName } from './findPageByName.js';
|
|
1
2
|
import { normalize } from './normalize.js';
|
|
2
3
|
export function getPageMessageId(pageName) {
|
|
3
4
|
return `pages.${normalize(pageName)}`;
|
|
@@ -35,4 +36,44 @@ export function findPageById(pages, normalizedPageId, appMessageIds, getAppMessa
|
|
|
35
36
|
}
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve which parent page applies to a member.
|
|
41
|
+
*
|
|
42
|
+
* @param parent The `parent` of a page, in any of its accepted forms.
|
|
43
|
+
* @param appMemberRoles The roles held by the app member.
|
|
44
|
+
* @returns The name of the applicable parent page, if any.
|
|
45
|
+
*/
|
|
46
|
+
function resolveParentName(parent, appMemberRoles) {
|
|
47
|
+
for (const entry of [].concat(parent ?? [])) {
|
|
48
|
+
if (typeof entry === 'string') {
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
if (!entry.roles || entry.roles.some((role) => appMemberRoles.includes(role))) {
|
|
52
|
+
return entry.page;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Collect the pages a page sits under, by following `parent` upwards.
|
|
58
|
+
*
|
|
59
|
+
* @param pages The pages of the app.
|
|
60
|
+
* @param page The page to collect the ancestors of.
|
|
61
|
+
* @param appMemberRoles The roles held by the app member, used to resolve role-scoped parents.
|
|
62
|
+
* @returns The ancestors, root first, excluding the page itself.
|
|
63
|
+
*/
|
|
64
|
+
export function getPageAncestors(pages, page, appMemberRoles = []) {
|
|
65
|
+
const ancestors = [];
|
|
66
|
+
const visited = new Set([page.name]);
|
|
67
|
+
let current = page;
|
|
68
|
+
for (let parentName = resolveParentName(current.parent, appMemberRoles); parentName; parentName = resolveParentName(current.parent, appMemberRoles)) {
|
|
69
|
+
const parent = findPageByName(pages, parentName);
|
|
70
|
+
if (!parent || visited.has(parent.name)) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
visited.add(parent.name);
|
|
74
|
+
ancestors.unshift(parent);
|
|
75
|
+
current = parent;
|
|
76
|
+
}
|
|
77
|
+
return ancestors;
|
|
78
|
+
}
|
|
38
79
|
//# sourceMappingURL=pageUtils.js.map
|
|
@@ -91,6 +91,15 @@ rendered as side navigation.
|
|
|
91
91
|
type: 'string',
|
|
92
92
|
enum: ['appName', 'pageName'],
|
|
93
93
|
},
|
|
94
|
+
breadcrumbs: {
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
default: false,
|
|
97
|
+
description: `Whether to display a breadcrumb trail below the title bar.
|
|
98
|
+
|
|
99
|
+
The trail is built from the \`parent\` of each page. A grid layout that names a \`breadcrumbs\`
|
|
100
|
+
template area renders the trail in that area instead.
|
|
101
|
+
`,
|
|
102
|
+
},
|
|
94
103
|
hideTitleBar: {
|
|
95
104
|
description: 'Whether to hide the title bar',
|
|
96
105
|
type: 'boolean',
|
|
@@ -104,6 +113,20 @@ Only applies when \`navigation\` is set to \`top\`.
|
|
|
104
113
|
type: 'boolean',
|
|
105
114
|
default: false,
|
|
106
115
|
},
|
|
116
|
+
navbar: {
|
|
117
|
+
$ref: '#/components/schemas/NavbarLayoutDefinition',
|
|
118
|
+
description: `Responsive grid layout for the top navigation header.
|
|
119
|
+
|
|
120
|
+
Only applies when \`navigation\` is set to \`top\` and cannot be combined with \`stackedHeader\`.
|
|
121
|
+
`,
|
|
122
|
+
},
|
|
123
|
+
breakpoints: {
|
|
124
|
+
$ref: '#/components/schemas/GridBreakpointsDefinition',
|
|
125
|
+
description: `Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
|
|
126
|
+
|
|
127
|
+
Applies to every responsive grid layout in the app, including pages and the navbar.
|
|
128
|
+
`,
|
|
129
|
+
},
|
|
107
130
|
hideGroupDropdown: {
|
|
108
131
|
description: `Whether to hide the group dropdown.
|
|
109
132
|
|
|
@@ -53,6 +53,25 @@ navigation. Set to \`hidden\` to display no navigational menus at all.
|
|
|
53
53
|
This will be displayed in the navigation menu.
|
|
54
54
|
`,
|
|
55
55
|
},
|
|
56
|
+
parent: {
|
|
57
|
+
description: `The page this page sits under in the breadcrumb hierarchy.
|
|
58
|
+
|
|
59
|
+
A page name applies to every app member. An entry carrying \`roles\` applies only to members holding
|
|
60
|
+
one of those roles. A list is resolved in order, so the first matching entry wins and an entry
|
|
61
|
+
without \`roles\` acts as the fallback.
|
|
62
|
+
|
|
63
|
+
A page that declares \`parameters\` cannot be used as a parent, because its URL cannot be resolved
|
|
64
|
+
from another page.
|
|
65
|
+
`,
|
|
66
|
+
anyOf: [
|
|
67
|
+
{ $ref: '#/components/schemas/PageParentDefinition' },
|
|
68
|
+
{
|
|
69
|
+
type: 'array',
|
|
70
|
+
minItems: 1,
|
|
71
|
+
items: { $ref: '#/components/schemas/PageParentDefinition' },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
56
75
|
parameters: {
|
|
57
76
|
type: 'array',
|
|
58
77
|
description: `Page parameters can be used for linking to a page that should display a single resource.
|
|
@@ -91,7 +91,10 @@ The exact meaning of the parameters depends on the block type.
|
|
|
91
91
|
events: { $ref: '#/components/schemas/EventsDefinition' },
|
|
92
92
|
gridArea: {
|
|
93
93
|
type: 'string',
|
|
94
|
-
description:
|
|
94
|
+
description: `Grid area defined in the page layout definition.
|
|
95
|
+
|
|
96
|
+
The \`breadcrumbs\` area is reserved for the breadcrumb trail of the app.
|
|
97
|
+
`,
|
|
95
98
|
},
|
|
96
99
|
},
|
|
97
100
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const GridBreakpointsDefinition = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
description: `Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
|
|
4
|
+
|
|
5
|
+
The mobile layout always applies from a width of 0. Each breakpoint must be smaller than the next.
|
|
6
|
+
`,
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
minProperties: 1,
|
|
9
|
+
properties: {
|
|
10
|
+
tablet: {
|
|
11
|
+
type: 'integer',
|
|
12
|
+
minimum: 0,
|
|
13
|
+
description: 'Minimum viewport width in pixels at which the tablet grid layout applies.',
|
|
14
|
+
},
|
|
15
|
+
desktop: {
|
|
16
|
+
type: 'integer',
|
|
17
|
+
minimum: 0,
|
|
18
|
+
description: 'Minimum viewport width in pixels at which the desktop grid layout applies.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=GridBreakpointsDefinition.js.map
|
|
@@ -5,9 +5,9 @@ export const GridSpacingDefinition = {
|
|
|
5
5
|
properties: {
|
|
6
6
|
unit: {
|
|
7
7
|
type: 'string',
|
|
8
|
-
description: 'CSS unit for spacing calculations (e.g., "1rem", "8px")',
|
|
8
|
+
description: 'CSS unit or custom property for spacing calculations (e.g., "1rem", "8px", "var(--grid-unit)")',
|
|
9
9
|
default: '1rem',
|
|
10
|
-
pattern: '
|
|
10
|
+
pattern: '^(?:-?(\\d+\\.?\\d*|\\.\\d+)(rem|em|px|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)|var\\(--[A-Za-z_][A-Za-z0-9_-]*\\))$',
|
|
11
11
|
},
|
|
12
12
|
gap: {
|
|
13
13
|
type: 'number',
|
|
@@ -27,6 +27,14 @@ By default the flow page retains it's data after navigating once. Set to false t
|
|
|
27
27
|
`,
|
|
28
28
|
default: true,
|
|
29
29
|
},
|
|
30
|
+
start: {
|
|
31
|
+
$ref: '#/components/schemas/SubPage',
|
|
32
|
+
description: 'A page to display before the generated loop pages.',
|
|
33
|
+
},
|
|
34
|
+
end: {
|
|
35
|
+
$ref: '#/components/schemas/SubPage',
|
|
36
|
+
description: 'A page to display after the generated loop pages.',
|
|
37
|
+
},
|
|
30
38
|
},
|
|
31
39
|
});
|
|
32
40
|
//# sourceMappingURL=LoopPageDefinition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const NavbarDeviceGridLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DeviceGridLayoutDefinition } from './DeviceGridLayoutDefinition.js';
|
|
2
|
+
import { extendJSONSchema } from './utils/extendJSONSchema.js';
|
|
3
|
+
export const NavbarDeviceGridLayoutDefinition = extendJSONSchema(DeviceGridLayoutDefinition, {
|
|
4
|
+
type: 'object',
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
required: ['layout'],
|
|
7
|
+
description: 'Grid layout definition for a navbar breakpoint',
|
|
8
|
+
});
|
|
9
|
+
//# sourceMappingURL=NavbarDeviceGridLayoutDefinition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const NavbarLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResponsiveGridLayoutDefinition } from './ResponsiveGridLayoutDefinition.js';
|
|
2
|
+
import { extendJSONSchema } from './utils/extendJSONSchema.js';
|
|
3
|
+
export const NavbarLayoutDefinition = extendJSONSchema(ResponsiveGridLayoutDefinition, {
|
|
4
|
+
type: 'object',
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
minProperties: 1,
|
|
7
|
+
description: `Responsive grid layout for the top navigation header.
|
|
8
|
+
|
|
9
|
+
The available template areas are logo, name, navigation, and controls.
|
|
10
|
+
Each rendered area must be included and their first visual occurrence must follow that order.
|
|
11
|
+
`,
|
|
12
|
+
properties: {
|
|
13
|
+
desktop: {
|
|
14
|
+
$ref: '#/components/schemas/NavbarDeviceGridLayoutDefinition',
|
|
15
|
+
},
|
|
16
|
+
mobile: {
|
|
17
|
+
$ref: '#/components/schemas/NavbarDeviceGridLayoutDefinition',
|
|
18
|
+
},
|
|
19
|
+
tablet: {
|
|
20
|
+
$ref: '#/components/schemas/NavbarDeviceGridLayoutDefinition',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=NavbarLayoutDefinition.js.map
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const PageLayoutDefinition: OpenAPIV3.NonArraySchemaObject;
|
|
1
|
+
export declare const PageLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { ResponsiveGridLayoutDefinition } from './ResponsiveGridLayoutDefinition.js';
|
|
2
|
+
import { extendJSONSchema } from './utils/extendJSONSchema.js';
|
|
3
|
+
export const PageLayoutDefinition = extendJSONSchema(ResponsiveGridLayoutDefinition, {
|
|
2
4
|
type: 'object',
|
|
3
|
-
description: 'Grid layout of the page, define as an anchor to re-use across multiple pages',
|
|
4
5
|
additionalProperties: false,
|
|
5
6
|
minProperties: 1,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
desktop: {
|
|
14
|
-
$ref: '#/components/schemas/DeviceGridLayoutDefinition',
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
};
|
|
7
|
+
description: `Responsive grid layout for a page.
|
|
8
|
+
|
|
9
|
+
The \`breadcrumbs\` template area is reserved for the breadcrumb trail of the app. It requires
|
|
10
|
+
\`layout.breadcrumbs\`, has to be part of the grid every breakpoint renders, and has to be defined by
|
|
11
|
+
every sub page of a page or by none of them.
|
|
12
|
+
`,
|
|
13
|
+
});
|
|
18
14
|
//# sourceMappingURL=PageLayoutDefinition.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const PageParentDefinition = {
|
|
2
|
+
description: `A candidate parent page, optionally scoped to a set of roles.
|
|
3
|
+
|
|
4
|
+
A plain string names the parent page. An object scopes that parent to the roles that may use it.
|
|
5
|
+
`,
|
|
6
|
+
anyOf: [
|
|
7
|
+
{
|
|
8
|
+
type: 'string',
|
|
9
|
+
maxLength: 50,
|
|
10
|
+
description: 'The name of the parent page.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
required: ['page'],
|
|
16
|
+
properties: {
|
|
17
|
+
page: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
maxLength: 50,
|
|
20
|
+
description: 'The name of the parent page.',
|
|
21
|
+
},
|
|
22
|
+
roles: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
minItems: 1,
|
|
25
|
+
items: { type: 'string' },
|
|
26
|
+
description: `The roles this parent applies to.
|
|
27
|
+
|
|
28
|
+
When omitted, the parent applies to every app member.
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=PageParentDefinition.js.map
|
|
@@ -100,10 +100,10 @@ name of the resource and how it should behave.
|
|
|
100
100
|
},
|
|
101
101
|
enforceOrderingGroupByFields: {
|
|
102
102
|
type: 'array',
|
|
103
|
-
pattern: '^[a-zA-Z0-9]*$',
|
|
104
103
|
description: 'Group resources by fields when handling resource positions',
|
|
105
104
|
items: {
|
|
106
105
|
type: 'string',
|
|
106
|
+
pattern: '^[a-zA-Z0-9]*$',
|
|
107
107
|
},
|
|
108
108
|
minItems: 1,
|
|
109
109
|
},
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const ResponsiveGridLayoutDefinition = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
description: 'Responsive grid layout',
|
|
4
|
+
additionalProperties: false,
|
|
5
|
+
minProperties: 1,
|
|
6
|
+
properties: {
|
|
7
|
+
mobile: {
|
|
8
|
+
$ref: '#/components/schemas/DeviceGridLayoutDefinition',
|
|
9
|
+
},
|
|
10
|
+
tablet: {
|
|
11
|
+
$ref: '#/components/schemas/DeviceGridLayoutDefinition',
|
|
12
|
+
},
|
|
13
|
+
desktop: {
|
|
14
|
+
$ref: '#/components/schemas/DeviceGridLayoutDefinition',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=ResponsiveGridLayoutDefinition.js.map
|
package/schemas/index.d.ts
CHANGED
|
@@ -124,6 +124,11 @@ export * from './TabsPageDefinition.js';
|
|
|
124
124
|
export * from './Theme.js';
|
|
125
125
|
export * from './WebhookDefinition.js';
|
|
126
126
|
export * from './DeviceGridLayoutDefinition.js';
|
|
127
|
+
export * from './GridBreakpointsDefinition.js';
|
|
127
128
|
export * from './GridLayoutDefinition.js';
|
|
128
129
|
export * from './GridSpacingDefinition.js';
|
|
130
|
+
export * from './ResponsiveGridLayoutDefinition.js';
|
|
131
|
+
export * from './NavbarDeviceGridLayoutDefinition.js';
|
|
132
|
+
export * from './NavbarLayoutDefinition.js';
|
|
129
133
|
export * from './PageLayoutDefinition.js';
|
|
134
|
+
export * from './PageParentDefinition.js';
|
package/schemas/index.js
CHANGED
|
@@ -124,7 +124,12 @@ export * from './TabsPageDefinition.js';
|
|
|
124
124
|
export * from './Theme.js';
|
|
125
125
|
export * from './WebhookDefinition.js';
|
|
126
126
|
export * from './DeviceGridLayoutDefinition.js';
|
|
127
|
+
export * from './GridBreakpointsDefinition.js';
|
|
127
128
|
export * from './GridLayoutDefinition.js';
|
|
128
129
|
export * from './GridSpacingDefinition.js';
|
|
130
|
+
export * from './ResponsiveGridLayoutDefinition.js';
|
|
131
|
+
export * from './NavbarDeviceGridLayoutDefinition.js';
|
|
132
|
+
export * from './NavbarLayoutDefinition.js';
|
|
129
133
|
export * from './PageLayoutDefinition.js';
|
|
134
|
+
export * from './PageParentDefinition.js';
|
|
130
135
|
//# sourceMappingURL=index.js.map
|
package/types/core.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ResourceDefinition } from './resource.js';
|
|
|
9
9
|
import { type AppRole, type ViewRole } from './roles.js';
|
|
10
10
|
import { type Security } from './security.js';
|
|
11
11
|
import { type Theme } from './theme.js';
|
|
12
|
-
import { type PageLayoutDefinition } from './gridLayout.js';
|
|
12
|
+
import { type GridBreakpointsDefinition, type NavbarLayoutDefinition, type PageLayoutDefinition } from './gridLayout.js';
|
|
13
13
|
export type SettingName = 'email' | 'languages' | 'name' | 'password' | 'phoneNumber' | 'picture';
|
|
14
14
|
export type AppContentSecurityPolicy = Record<string, string[]>;
|
|
15
15
|
export interface AppDefinition {
|
|
@@ -108,6 +108,10 @@ export interface AppDefinition {
|
|
|
108
108
|
*
|
|
109
109
|
*/
|
|
110
110
|
titleBarText?: 'appName' | 'pageName';
|
|
111
|
+
/**
|
|
112
|
+
* Whether to display a breadcrumb trail below the title bar.
|
|
113
|
+
*/
|
|
114
|
+
breadcrumbs?: boolean;
|
|
111
115
|
/**
|
|
112
116
|
* Whether to hide the title bar from all pages in the app.
|
|
113
117
|
*/
|
|
@@ -118,6 +122,24 @@ export interface AppDefinition {
|
|
|
118
122
|
* Only applies when `navigation` is set to `top`.
|
|
119
123
|
*/
|
|
120
124
|
stackedHeader?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Responsive grid layout for the top navigation header.
|
|
127
|
+
*
|
|
128
|
+
* Only applies when `navigation` is set to `top`. This cannot be combined with
|
|
129
|
+
* `stackedHeader`.
|
|
130
|
+
*
|
|
131
|
+
* The available template areas are logo, name, navigation, and controls.
|
|
132
|
+
* Each rendered area must be included and their first visual occurrence must follow that order.
|
|
133
|
+
*/
|
|
134
|
+
navbar?: NavbarLayoutDefinition;
|
|
135
|
+
/**
|
|
136
|
+
* Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
|
|
137
|
+
*
|
|
138
|
+
* Applies to every responsive grid layout in the app, including pages and the navbar. The
|
|
139
|
+
* mobile layout always applies from a width of 0. Each breakpoint must be smaller than the
|
|
140
|
+
* next.
|
|
141
|
+
*/
|
|
142
|
+
breakpoints?: GridBreakpointsDefinition;
|
|
121
143
|
/**
|
|
122
144
|
* Whether to hide the group dropdown.
|
|
123
145
|
*
|
|
@@ -181,6 +203,13 @@ export interface AppDefinition {
|
|
|
181
203
|
*/
|
|
182
204
|
registry?: string;
|
|
183
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* A candidate parent page, optionally scoped to a set of roles.
|
|
208
|
+
*/
|
|
209
|
+
export type PageParentDefinition = string | {
|
|
210
|
+
page: string;
|
|
211
|
+
roles?: string[];
|
|
212
|
+
};
|
|
184
213
|
/**
|
|
185
214
|
* This describes what a page will look like in the app.
|
|
186
215
|
*/
|
|
@@ -215,6 +244,14 @@ export interface BasePageDefinition {
|
|
|
215
244
|
* top navigation is rendered as side navigation.
|
|
216
245
|
*/
|
|
217
246
|
navigation?: Navigation | 'profileDropdown';
|
|
247
|
+
/**
|
|
248
|
+
* The page this page sits under in the breadcrumb hierarchy.
|
|
249
|
+
*
|
|
250
|
+
* A page name applies to every member. An entry carrying `roles` applies only to members holding
|
|
251
|
+
* one of them. A list is resolved in order, so the first matching entry wins and an entry without
|
|
252
|
+
* `roles` acts as the fallback.
|
|
253
|
+
*/
|
|
254
|
+
parent?: PageParentDefinition | PageParentDefinition[];
|
|
218
255
|
/**
|
|
219
256
|
* A list of roles that may view the page.
|
|
220
257
|
*/
|
|
@@ -316,6 +353,14 @@ export interface LoopPageDefinition extends BasePageDefinition {
|
|
|
316
353
|
* @default true
|
|
317
354
|
*/
|
|
318
355
|
retainFlowData?: boolean;
|
|
356
|
+
/**
|
|
357
|
+
* The page to display before the generated loop pages.
|
|
358
|
+
*/
|
|
359
|
+
start?: SubPageDefinition;
|
|
360
|
+
/**
|
|
361
|
+
* The page to display after the generated loop pages.
|
|
362
|
+
*/
|
|
363
|
+
end?: SubPageDefinition;
|
|
319
364
|
}
|
|
320
365
|
export interface AlternateTabsDefinition {
|
|
321
366
|
foreach: SubPageDefinition;
|
package/types/gridLayout.d.ts
CHANGED
|
@@ -8,12 +8,26 @@ export interface GridLayoutSpacingDefinition {
|
|
|
8
8
|
padding: number;
|
|
9
9
|
gap: number;
|
|
10
10
|
}
|
|
11
|
+
export interface GridBreakpointsDefinition {
|
|
12
|
+
tablet?: number;
|
|
13
|
+
desktop?: number;
|
|
14
|
+
}
|
|
11
15
|
export interface DeviceGridLayoutDefinition {
|
|
16
|
+
layout?: GridLayoutDefinition;
|
|
17
|
+
spacing?: GridLayoutSpacingDefinition;
|
|
18
|
+
}
|
|
19
|
+
export interface ResponsiveGridLayoutDefinition {
|
|
20
|
+
desktop?: DeviceGridLayoutDefinition;
|
|
21
|
+
mobile?: DeviceGridLayoutDefinition;
|
|
22
|
+
tablet?: DeviceGridLayoutDefinition;
|
|
23
|
+
}
|
|
24
|
+
export interface NavbarDeviceGridLayoutDefinition extends DeviceGridLayoutDefinition {
|
|
12
25
|
layout: GridLayoutDefinition;
|
|
13
|
-
spacing: GridLayoutSpacingDefinition;
|
|
14
26
|
}
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
27
|
+
export interface ResponsiveNavbarGridLayoutDefinition {
|
|
28
|
+
desktop?: NavbarDeviceGridLayoutDefinition;
|
|
29
|
+
mobile?: NavbarDeviceGridLayoutDefinition;
|
|
30
|
+
tablet?: NavbarDeviceGridLayoutDefinition;
|
|
31
|
+
}
|
|
32
|
+
export type NavbarLayoutDefinition = RequireAtLeastOne<ResponsiveNavbarGridLayoutDefinition>;
|
|
33
|
+
export type PageLayoutDefinition = RequireAtLeastOne<ResponsiveGridLayoutDefinition>;
|
package/validation.js
CHANGED
|
@@ -6,6 +6,7 @@ import { BlockParamInstanceValidator } from './BasicValidator.js';
|
|
|
6
6
|
import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
|
|
7
7
|
import { partialNormalized } from './constants/index.js';
|
|
8
8
|
import { findPageByName } from './findPageByName.js';
|
|
9
|
+
import { breadcrumbsGridArea, getCascadedGridTemplateAreas, getGridTemplateAreas, gridDeviceOrder, } from './gridUtils.js';
|
|
9
10
|
import { iterAction, iterApp } from './iterApp.js';
|
|
10
11
|
import { has } from './miscellaneous.js';
|
|
11
12
|
import { normalize } from './normalize.js';
|
|
@@ -109,6 +110,70 @@ function validateUniquePageNames(definition, report) {
|
|
|
109
110
|
}
|
|
110
111
|
checkPages(definition.pages);
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Iterate over the parent entries of every page, including the pages nested in a container.
|
|
115
|
+
*
|
|
116
|
+
* `iterApp` gives a nested page the path of the container it sits in, which is too coarse to point
|
|
117
|
+
* at the `parent` of one page, so the pages are walked here instead.
|
|
118
|
+
*
|
|
119
|
+
* @param pages The pages to walk.
|
|
120
|
+
* @param onParent Called with each parent entry, the path it lives at, and the page declaring it.
|
|
121
|
+
* @param prefix The path the pages live at.
|
|
122
|
+
*/
|
|
123
|
+
function iterPageParents(pages, onParent, prefix = ['pages']) {
|
|
124
|
+
for (const [index, page] of pages.entries()) {
|
|
125
|
+
const pagePath = [...prefix, index];
|
|
126
|
+
for (const [entryIndex, entry] of []
|
|
127
|
+
.concat(page.parent ?? [])
|
|
128
|
+
.entries()) {
|
|
129
|
+
onParent(entry, Array.isArray(page.parent) ? [...pagePath, 'parent', entryIndex] : [...pagePath, 'parent'], page);
|
|
130
|
+
}
|
|
131
|
+
if (page.type === 'container') {
|
|
132
|
+
iterPageParents(page.pages, onParent, [...pagePath, 'pages']);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function validatePageParents(definition, report) {
|
|
137
|
+
if (!definition.pages) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Every declared edge counts, whatever roles it is scoped to: roles overlap, so a cycle reachable
|
|
141
|
+
// under any role assignment is an authoring mistake.
|
|
142
|
+
function referencesItself(pageName, parentName) {
|
|
143
|
+
const visited = new Set();
|
|
144
|
+
const queue = [parentName];
|
|
145
|
+
while (queue.length) {
|
|
146
|
+
const name = queue.shift();
|
|
147
|
+
if (name === pageName) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (visited.has(name)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
visited.add(name);
|
|
154
|
+
const parent = findPageByName(definition.pages, name);
|
|
155
|
+
for (const entry of [].concat(parent?.parent ?? [])) {
|
|
156
|
+
queue.push(typeof entry === 'string' ? entry : entry.page);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
iterPageParents(definition.pages, (entry, path, page) => {
|
|
162
|
+
const parentName = typeof entry === 'string' ? entry : entry.page;
|
|
163
|
+
const parent = findPageByName(definition.pages, parentName);
|
|
164
|
+
if (!parent) {
|
|
165
|
+
report(parentName, 'refers to a page that doesn’t exist', path);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (referencesItself(page.name, parentName)) {
|
|
169
|
+
report(parentName, 'cyclically references itself', path);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (parent.parameters?.length) {
|
|
173
|
+
report(parentName, 'refers to a page with parameters, which cannot be used as a parent', path);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
112
177
|
function validateMembersSchema(definition, report) {
|
|
113
178
|
if (!definition.members?.properties) {
|
|
114
179
|
return;
|
|
@@ -368,24 +433,6 @@ function validateController(definition, controllerImplementations, report) {
|
|
|
368
433
|
},
|
|
369
434
|
});
|
|
370
435
|
}
|
|
371
|
-
function getTemplateAreas(layoutDefinition) {
|
|
372
|
-
const areas = new Set();
|
|
373
|
-
if (!layoutDefinition) {
|
|
374
|
-
return areas;
|
|
375
|
-
}
|
|
376
|
-
for (const deviceDefinition of Object.values(layoutDefinition)) {
|
|
377
|
-
if (deviceDefinition?.layout?.template) {
|
|
378
|
-
for (const row of deviceDefinition.layout.template) {
|
|
379
|
-
for (const area of row.split(' ')) {
|
|
380
|
-
if (area && area !== '.') {
|
|
381
|
-
areas.add(area);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return areas;
|
|
388
|
-
}
|
|
389
436
|
function validateTemplateAreasAreRectangular(template, report, path) {
|
|
390
437
|
const grid = template.map((row) => row.split(' '));
|
|
391
438
|
function isAreaRectangular(areaName) {
|
|
@@ -437,7 +484,7 @@ function validateTemplateAreasAreRectangular(template, report, path) {
|
|
|
437
484
|
}
|
|
438
485
|
}
|
|
439
486
|
}
|
|
440
|
-
function
|
|
487
|
+
function validateResponsiveGridLayoutDefinition(layoutDefinition, report, path) {
|
|
441
488
|
if (!layoutDefinition) {
|
|
442
489
|
return;
|
|
443
490
|
}
|
|
@@ -445,10 +492,16 @@ function validatePageLayoutDefinition(layoutDefinition, report, path) {
|
|
|
445
492
|
if (!deviceDefinition?.layout?.template || !deviceDefinition?.layout?.columns) {
|
|
446
493
|
continue;
|
|
447
494
|
}
|
|
448
|
-
|
|
449
|
-
|
|
495
|
+
const { layout } = deviceDefinition;
|
|
496
|
+
if (layout.template.some((row) => row.split(' ').length !== layout.columns)) {
|
|
497
|
+
report(layout.template, 'template needs to be the same length as number of columns', [
|
|
498
|
+
...path,
|
|
499
|
+
deviceName,
|
|
500
|
+
'layout',
|
|
501
|
+
'template',
|
|
502
|
+
]);
|
|
450
503
|
}
|
|
451
|
-
validateTemplateAreasAreRectangular(
|
|
504
|
+
validateTemplateAreasAreRectangular(layout.template, report, [
|
|
452
505
|
...path,
|
|
453
506
|
deviceName,
|
|
454
507
|
'layout',
|
|
@@ -457,56 +510,163 @@ function validatePageLayoutDefinition(layoutDefinition, report, path) {
|
|
|
457
510
|
}
|
|
458
511
|
}
|
|
459
512
|
function validateBlockGridAreas(blocks, layoutDefinition, report, path) {
|
|
460
|
-
const templateAreas =
|
|
513
|
+
const templateAreas = getGridTemplateAreas(layoutDefinition);
|
|
461
514
|
for (const [idx, block] of blocks.entries()) {
|
|
462
|
-
if (block.gridArea
|
|
515
|
+
if (block.gridArea === breadcrumbsGridArea) {
|
|
516
|
+
report(block.gridArea, 'is reserved for the breadcrumb trail', [...path, idx, 'gridArea']);
|
|
517
|
+
}
|
|
518
|
+
else if (layoutDefinition && block.gridArea && !templateAreas.has(block.gridArea)) {
|
|
463
519
|
report(block.gridArea, `does not match any area defined in the layout template. Available areas: ${[...templateAreas].join(', ') || 'none'}`, [...path, idx, 'gridArea']);
|
|
464
520
|
}
|
|
465
521
|
}
|
|
466
522
|
}
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
|
|
523
|
+
function validateBreadcrumbsGridArea(layout, report, path, breadcrumbsEnabled) {
|
|
524
|
+
if (!layout || !getGridTemplateAreas(layout).has(breadcrumbsGridArea)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (!breadcrumbsEnabled) {
|
|
528
|
+
for (const [deviceName, deviceDefinition] of Object.entries(layout)) {
|
|
529
|
+
const template = deviceDefinition?.layout?.template;
|
|
530
|
+
if (template?.some((row) => row.split(' ').includes(breadcrumbsGridArea))) {
|
|
531
|
+
report(template, `the '${breadcrumbsGridArea}' grid area requires layout.breadcrumbs to be enabled`, [...path, deviceName, 'layout', 'template']);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// A device inherits the template of the next smaller one, so an area defined only for a larger
|
|
537
|
+
// device leaves the trail without a slot to sit in below that breakpoint.
|
|
538
|
+
const cascaded = getCascadedGridTemplateAreas(layout);
|
|
539
|
+
const missing = gridDeviceOrder.filter((device) => !cascaded[device].has(breadcrumbsGridArea));
|
|
540
|
+
if (missing.length) {
|
|
541
|
+
report(layout, `the '${breadcrumbsGridArea}' grid area is missing from the grid rendered on ${missing.join(', ')}`, path);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function validateConsistentBreadcrumbsGridArea(subPages, report) {
|
|
545
|
+
// The trail belongs to the page, so it may not appear on one sub page and be missing from the
|
|
546
|
+
// next. Defining the area consistently also lets the app runtime tell from the page definition
|
|
547
|
+
// alone whether a page renders the trail from its grid or below the title bar.
|
|
548
|
+
const declaresArea = ([subPage]) => getGridTemplateAreas(subPage.layout).has(breadcrumbsGridArea);
|
|
549
|
+
const declaring = subPages.filter(declaresArea);
|
|
550
|
+
if (!declaring.length || declaring.length === subPages.length) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
for (const entry of subPages) {
|
|
554
|
+
if (!declaresArea(entry)) {
|
|
555
|
+
report(entry[0], `the '${breadcrumbsGridArea}' grid area must be defined by every sub page of this page or by none`, entry[1]);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function validateSubPageLayout(layout, blocks, report, path, breadcrumbsEnabled) {
|
|
560
|
+
validateResponsiveGridLayoutDefinition(layout, report, path);
|
|
561
|
+
validateBreadcrumbsGridArea(layout, report, path, breadcrumbsEnabled);
|
|
562
|
+
if (blocks) {
|
|
470
563
|
validateBlockGridAreas(blocks, layout, report, [...path, 'blocks']);
|
|
471
564
|
}
|
|
472
565
|
}
|
|
473
566
|
function validateGridLayout(definition, report) {
|
|
567
|
+
const breakpoints = definition.layout?.breakpoints;
|
|
568
|
+
if (breakpoints?.tablet != null &&
|
|
569
|
+
breakpoints?.desktop != null &&
|
|
570
|
+
breakpoints.tablet >= breakpoints.desktop) {
|
|
571
|
+
report(breakpoints, 'tablet breakpoint must be smaller than the desktop breakpoint', [
|
|
572
|
+
'layout',
|
|
573
|
+
'breakpoints',
|
|
574
|
+
]);
|
|
575
|
+
}
|
|
576
|
+
const navbarLayout = definition.layout?.navbar;
|
|
577
|
+
validateResponsiveGridLayoutDefinition(navbarLayout, report, ['layout', 'navbar']);
|
|
578
|
+
if (navbarLayout) {
|
|
579
|
+
if (definition.layout?.navigation !== 'top') {
|
|
580
|
+
report(navbarLayout, 'only applies when navigation is set to top', ['layout', 'navbar']);
|
|
581
|
+
}
|
|
582
|
+
if (definition.layout?.stackedHeader) {
|
|
583
|
+
report(navbarLayout, 'cannot be combined with stackedHeader', ['layout', 'navbar']);
|
|
584
|
+
}
|
|
585
|
+
const navbarAreaOrder = ['logo', 'name', 'navigation', 'controls'];
|
|
586
|
+
const allowedAreas = new Set(['.', ...navbarAreaOrder]);
|
|
587
|
+
const requiredAreas = definition.layout?.logo?.position === 'navbar' ? navbarAreaOrder : navbarAreaOrder.slice(1);
|
|
588
|
+
for (const [deviceName, deviceDefinition] of Object.entries(navbarLayout)) {
|
|
589
|
+
if (!deviceDefinition?.layout?.template) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const { template } = deviceDefinition.layout;
|
|
593
|
+
const templatePath = ['layout', 'navbar', deviceName, 'layout', 'template'];
|
|
594
|
+
const visualAreaOrder = [];
|
|
595
|
+
const visualAreas = new Set();
|
|
596
|
+
for (const [rowIndex, row] of deviceDefinition.layout.template.entries()) {
|
|
597
|
+
for (const area of row.split(' ')) {
|
|
598
|
+
if (!allowedAreas.has(area)) {
|
|
599
|
+
report(row, `unknown navbar grid area '${area}'`, [...templatePath, rowIndex]);
|
|
600
|
+
}
|
|
601
|
+
else if (area !== '.' && !visualAreas.has(area)) {
|
|
602
|
+
visualAreas.add(area);
|
|
603
|
+
visualAreaOrder.push(area);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
let hasMissingArea = false;
|
|
608
|
+
for (const area of requiredAreas) {
|
|
609
|
+
if (!visualAreas.has(area)) {
|
|
610
|
+
hasMissingArea = true;
|
|
611
|
+
report(template, `navbar grid is missing required area '${area}'`, templatePath);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (!hasMissingArea) {
|
|
615
|
+
const expectedAreaOrder = navbarAreaOrder.filter((area) => visualAreas.has(area));
|
|
616
|
+
if (visualAreaOrder.some((area, index) => area !== expectedAreaOrder[index])) {
|
|
617
|
+
report(template, `navbar grid areas must follow the accessible order: ${expectedAreaOrder.join(', ')}`, templatePath);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const breadcrumbsEnabled = definition.layout?.breadcrumbs === true;
|
|
474
623
|
iterApp(definition, {
|
|
475
624
|
onPage(page, path) {
|
|
476
625
|
// Basic page
|
|
477
626
|
if (page.type === 'page' || page.type === undefined) {
|
|
478
627
|
const basicPage = page;
|
|
479
|
-
validateSubPageLayout(basicPage.layout, basicPage.blocks, report, path);
|
|
628
|
+
validateSubPageLayout(basicPage.layout, basicPage.blocks, report, path, breadcrumbsEnabled);
|
|
480
629
|
return;
|
|
481
630
|
}
|
|
482
631
|
// Tabs page
|
|
483
632
|
if (page.type === 'tabs') {
|
|
484
633
|
const tabsPage = page;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
validateSubPageLayout(
|
|
492
|
-
}
|
|
634
|
+
const tabs = tabsPage.tabs
|
|
635
|
+
? tabsPage.tabs.map((tab, tabId) => [tab, [...path, 'tabs', tabId]])
|
|
636
|
+
: tabsPage.definition?.foreach
|
|
637
|
+
? [[tabsPage.definition.foreach, [...path, 'definition', 'foreach']]]
|
|
638
|
+
: [];
|
|
639
|
+
for (const [tab, tabPath] of tabs) {
|
|
640
|
+
validateSubPageLayout(tab.layout, tab.blocks, report, tabPath, breadcrumbsEnabled);
|
|
641
|
+
}
|
|
642
|
+
validateConsistentBreadcrumbsGridArea(tabs, report);
|
|
493
643
|
return;
|
|
494
644
|
}
|
|
495
645
|
// Flow page
|
|
496
646
|
if (page.type === 'flow') {
|
|
497
647
|
const flowPage = page;
|
|
498
|
-
|
|
499
|
-
|
|
648
|
+
const steps = flowPage.steps.map((step, stepId) => [
|
|
649
|
+
step,
|
|
650
|
+
[...path, 'steps', stepId],
|
|
651
|
+
]);
|
|
652
|
+
for (const [step, stepPath] of steps) {
|
|
653
|
+
validateSubPageLayout(step.layout, step.blocks, report, stepPath, breadcrumbsEnabled);
|
|
500
654
|
}
|
|
655
|
+
validateConsistentBreadcrumbsGridArea(steps, report);
|
|
501
656
|
return;
|
|
502
657
|
}
|
|
503
658
|
// Loop page
|
|
504
659
|
if (page.type === 'loop') {
|
|
505
660
|
const loopPage = page;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
'foreach',
|
|
509
|
-
|
|
661
|
+
const subPages = [
|
|
662
|
+
[loopPage.start, 'start'],
|
|
663
|
+
[loopPage.foreach, 'foreach'],
|
|
664
|
+
[loopPage.end, 'end'],
|
|
665
|
+
].flatMap(([subPage, key]) => subPage ? [[subPage, [...path, key]]] : []);
|
|
666
|
+
for (const [subPage, subPagePath] of subPages) {
|
|
667
|
+
validateSubPageLayout(subPage.layout, subPage.blocks, report, subPagePath, breadcrumbsEnabled);
|
|
668
|
+
}
|
|
669
|
+
validateConsistentBreadcrumbsGridArea(subPages, report);
|
|
510
670
|
}
|
|
511
671
|
},
|
|
512
672
|
});
|
|
@@ -999,6 +1159,13 @@ function validateSecurity(definition, report) {
|
|
|
999
1159
|
}
|
|
1000
1160
|
}
|
|
1001
1161
|
iterApp(definition, { onBlock: checkRoles, onPage: checkRoles });
|
|
1162
|
+
if (definition.pages) {
|
|
1163
|
+
iterPageParents(definition.pages, (entry, path) => {
|
|
1164
|
+
if (typeof entry !== 'string') {
|
|
1165
|
+
checkRoles(entry, path);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1002
1169
|
const { hideGroupDropdown } = definition.layout ?? {};
|
|
1003
1170
|
if (Array.isArray(hideGroupDropdown)) {
|
|
1004
1171
|
for (const [index, role] of hideGroupDropdown.entries()) {
|
|
@@ -1503,6 +1670,7 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
|
|
|
1503
1670
|
validateLanguage(clonedDefinition, report);
|
|
1504
1671
|
validateResourceReferences(clonedDefinition, report);
|
|
1505
1672
|
validateMembersSchema(clonedDefinition, report);
|
|
1673
|
+
validatePageParents(clonedDefinition, report);
|
|
1506
1674
|
validatePhoneNumberDefinition(clonedDefinition, report);
|
|
1507
1675
|
validateResourceSchemas(clonedDefinition, report);
|
|
1508
1676
|
validateSecurity(clonedDefinition, report);
|