@protobase/layout 0.46.3 → 0.46.4
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 +49 -0
- package/dist/blocks.d.ts +38 -0
- package/dist/blocks.js +11 -1
- package/dist/check-account.d.ts +6 -0
- package/dist/check-account.js +25 -0
- package/dist/check-page.js +4 -0
- package/dist/check-structure.js +5 -4
- package/dist/check-tabs.js +36 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/page.js +2 -1
- package/dist/presets/account-page.d.ts +20 -0
- package/dist/presets/account-page.js +45 -0
- package/dist/prune-page.d.ts +3 -3
- package/dist/prune-page.js +17 -4
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +65 -0
- package/dist/walk.d.ts +5 -0
- package/package.json +2 -2
package/dist/block-specs.js
CHANGED
|
@@ -177,9 +177,58 @@ const blockSpecs = {
|
|
|
177
177
|
to: optional("address")
|
|
178
178
|
}
|
|
179
179
|
},
|
|
180
|
+
Tabs: {
|
|
181
|
+
children: true,
|
|
182
|
+
props: {}
|
|
183
|
+
},
|
|
184
|
+
Tab: {
|
|
185
|
+
children: true,
|
|
186
|
+
props: {
|
|
187
|
+
name: required("text"),
|
|
188
|
+
label: required("text")
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
Settings: {
|
|
192
|
+
children: false,
|
|
193
|
+
props: {
|
|
194
|
+
resource: required("text"),
|
|
195
|
+
fields: optional("texts"),
|
|
196
|
+
title: optional("text"),
|
|
197
|
+
description: optional("text"),
|
|
198
|
+
span: optional("span")
|
|
199
|
+
}
|
|
200
|
+
},
|
|
180
201
|
Users: {
|
|
181
202
|
children: false,
|
|
182
203
|
props: {}
|
|
204
|
+
},
|
|
205
|
+
Profile: {
|
|
206
|
+
children: false,
|
|
207
|
+
props: {}
|
|
208
|
+
},
|
|
209
|
+
Password: {
|
|
210
|
+
children: false,
|
|
211
|
+
props: {}
|
|
212
|
+
},
|
|
213
|
+
Passkeys: {
|
|
214
|
+
children: false,
|
|
215
|
+
props: {}
|
|
216
|
+
},
|
|
217
|
+
TwoFactor: {
|
|
218
|
+
children: false,
|
|
219
|
+
props: {}
|
|
220
|
+
},
|
|
221
|
+
ConnectedAccounts: {
|
|
222
|
+
children: false,
|
|
223
|
+
props: {}
|
|
224
|
+
},
|
|
225
|
+
Devices: {
|
|
226
|
+
children: false,
|
|
227
|
+
props: {}
|
|
228
|
+
},
|
|
229
|
+
ConnectedApps: {
|
|
230
|
+
children: false,
|
|
231
|
+
props: {}
|
|
183
232
|
}
|
|
184
233
|
};
|
|
185
234
|
const formats = [
|
package/dist/blocks.d.ts
CHANGED
|
@@ -183,8 +183,36 @@ export type LinkProps = ({
|
|
|
183
183
|
}) & {
|
|
184
184
|
children?: LayoutChildren;
|
|
185
185
|
};
|
|
186
|
+
/** One route per `Tab` below the page's address: the first tab at the page's own, each other at `/<page>/<tab name>`. */
|
|
187
|
+
export type TabsProps = {
|
|
188
|
+
children?: LayoutChildren;
|
|
189
|
+
};
|
|
190
|
+
/** One tab of a `Tabs`: `name` is its address, after the page's, `label` the word it shows. */
|
|
191
|
+
export type TabProps = {
|
|
192
|
+
name: string;
|
|
193
|
+
label: string;
|
|
194
|
+
children?: LayoutChildren;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* The signed-in person's own record of `resource`, a resource whose `.owner(...)` is its key, as settings: each of
|
|
198
|
+
* `fields` as a control that saves on change, its default until the first save creates the record. Every caller, an
|
|
199
|
+
* admin too, reads and writes only their own record of it. `fields` defaults to every field a person can set.
|
|
200
|
+
*/
|
|
201
|
+
export type SettingsProps = {
|
|
202
|
+
resource: string;
|
|
203
|
+
fields?: string[];
|
|
204
|
+
title?: string;
|
|
205
|
+
description?: string;
|
|
206
|
+
span?: GridSpan;
|
|
207
|
+
};
|
|
186
208
|
/** Everyone who can sign in, for admins: inviting by email, adding accounts, changing roles and blocking. Takes no props. */
|
|
187
209
|
export type UsersProps = Record<string, never>;
|
|
210
|
+
/**
|
|
211
|
+
* A part of the signed-in person's own account, for the `account` page only: their profile, password, passkeys,
|
|
212
|
+
* two-factor authentication, connected accounts or connected apps. Each takes no props, loads its own data, and hides
|
|
213
|
+
* itself where it does not apply, such as Connected apps without MCP.
|
|
214
|
+
*/
|
|
215
|
+
export type AccountBlockProps = Record<string, never>;
|
|
188
216
|
export declare const Page: Block<PageProps>;
|
|
189
217
|
export declare const Grid: Block<GridProps>;
|
|
190
218
|
export declare const Card: Block<CardProps>;
|
|
@@ -201,5 +229,15 @@ export declare const ModalForm: Block<ModalFormProps>;
|
|
|
201
229
|
export declare const Action: Block<ActionProps>;
|
|
202
230
|
export declare const Show: Block<ShowProps>;
|
|
203
231
|
export declare const Link: Block<LinkProps>;
|
|
232
|
+
export declare const Tabs: Block<TabsProps>;
|
|
233
|
+
export declare const Tab: Block<TabProps>;
|
|
234
|
+
export declare const Settings: Block<SettingsProps>;
|
|
204
235
|
export declare const Users: Block<UsersProps>;
|
|
236
|
+
export declare const Profile: Block<AccountBlockProps>;
|
|
237
|
+
export declare const Password: Block<AccountBlockProps>;
|
|
238
|
+
export declare const Passkeys: Block<AccountBlockProps>;
|
|
239
|
+
export declare const TwoFactor: Block<AccountBlockProps>;
|
|
240
|
+
export declare const ConnectedAccounts: Block<AccountBlockProps>;
|
|
241
|
+
export declare const Devices: Block<AccountBlockProps>;
|
|
242
|
+
export declare const ConnectedApps: Block<AccountBlockProps>;
|
|
205
243
|
//#endregion
|
package/dist/blocks.js
CHANGED
|
@@ -16,6 +16,16 @@ const ModalForm = defineBlock("ModalForm", false);
|
|
|
16
16
|
const Action = defineBlock("Action", false);
|
|
17
17
|
const Show = defineBlock("Show", false);
|
|
18
18
|
const Link = defineBlock("Link", false);
|
|
19
|
+
const Tabs = defineBlock("Tabs", false);
|
|
20
|
+
const Tab = defineBlock("Tab", false);
|
|
21
|
+
const Settings = defineBlock("Settings", false);
|
|
19
22
|
const Users = defineBlock("Users", false);
|
|
23
|
+
const Profile = defineBlock("Profile", false);
|
|
24
|
+
const Password = defineBlock("Password", false);
|
|
25
|
+
const Passkeys = defineBlock("Passkeys", false);
|
|
26
|
+
const TwoFactor = defineBlock("TwoFactor", false);
|
|
27
|
+
const ConnectedAccounts = defineBlock("ConnectedAccounts", false);
|
|
28
|
+
const Devices = defineBlock("Devices", false);
|
|
29
|
+
const ConnectedApps = defineBlock("ConnectedApps", false);
|
|
20
30
|
//#endregion
|
|
21
|
-
export { Action, Card, CardRow, Chart, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordCards, RecordRow, Show, Stat, Table, Users };
|
|
31
|
+
export { Action, Card, CardRow, Chart, ConnectedAccounts, ConnectedApps, Devices, Field, Grid, Link, ModalForm, Page, Passkeys, Password, Profile, Progress, RecordCard, RecordCards, RecordRow, Settings, Show, Stat, Tab, Table, Tabs, TwoFactor, Users };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/check-account.d.ts
|
|
2
|
+
/** The page that holds the signed-in person's own account. */
|
|
3
|
+
export declare const accountPageName = "account";
|
|
4
|
+
/** The blocks that act on the signed-in person's own sign-in, so they belong on the account page alone. */
|
|
5
|
+
export declare const accountBlocks: ReadonlySet<string>;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { walkTree } from "./walk.js";
|
|
2
|
+
//#region src/check-account.ts
|
|
3
|
+
/** The page that holds the signed-in person's own account. */
|
|
4
|
+
const accountPageName = "account";
|
|
5
|
+
/** The blocks that act on the signed-in person's own sign-in, so they belong on the account page alone. */
|
|
6
|
+
const accountBlocks = /* @__PURE__ */ new Set([
|
|
7
|
+
"Profile",
|
|
8
|
+
"Password",
|
|
9
|
+
"Passkeys",
|
|
10
|
+
"TwoFactor",
|
|
11
|
+
"ConnectedAccounts",
|
|
12
|
+
"Devices",
|
|
13
|
+
"ConnectedApps"
|
|
14
|
+
]);
|
|
15
|
+
/** Each account block on a page other than the account page, with where it is. */
|
|
16
|
+
const accountBlockProblems = (name, tree) => {
|
|
17
|
+
if (name === "account") return [];
|
|
18
|
+
const problems = [];
|
|
19
|
+
walkTree(tree, (node, { path }) => {
|
|
20
|
+
if (!node.custom && accountBlocks.has(node.type)) problems.push(`${path}: <${node.type}> acts on the signed-in person's own account, so it is only allowed on the "${accountPageName}" page`);
|
|
21
|
+
});
|
|
22
|
+
return problems;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { accountBlockProblems, accountBlocks, accountPageName };
|
package/dist/check-page.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dataProps } from "./node.js";
|
|
2
2
|
import { walkTree } from "./walk.js";
|
|
3
|
+
import { settingsProblems } from "./settings.js";
|
|
3
4
|
import { chartProblems } from "./check-chart.js";
|
|
4
5
|
import { resolveFieldPath } from "./field-path.js";
|
|
5
6
|
import { conditionProblems } from "./check-condition.js";
|
|
@@ -73,6 +74,9 @@ const nodeProblems = (node, scope, { models, actions, pages, createPages }) => {
|
|
|
73
74
|
}
|
|
74
75
|
for (const name of Object.keys(node.props.values ?? {})) if (!Object.hasOwn(model.fields, name)) problems.push(`values: unknown field "${name}" on ${model.name}`);
|
|
75
76
|
break;
|
|
77
|
+
case "Settings":
|
|
78
|
+
if (model) problems.push(...settingsProblems(model, node.props.fields));
|
|
79
|
+
break;
|
|
76
80
|
case "Action":
|
|
77
81
|
if (resource !== void 0 && !actions(resource).has(String(node.props.name))) problems.push(`no action "${node.props.name}" on ${resource}; declare it with view('${resource}').actions(...)`);
|
|
78
82
|
break;
|
package/dist/check-structure.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { blockSpecs, propProblems } from "./block-specs.js";
|
|
2
|
-
import { parseCondition } from "./condition.js";
|
|
3
2
|
import { walkTree } from "./walk.js";
|
|
3
|
+
import { parseCondition } from "./condition.js";
|
|
4
|
+
import { tabsProblems } from "./check-tabs.js";
|
|
4
5
|
import { appHrefError } from "@protobase/schema/internal";
|
|
5
6
|
//#region src/check-structure.ts
|
|
6
7
|
/** Problems that need a record around the element, which only a `RecordCard` or a `CardRow` card provides. */
|
|
@@ -33,8 +34,8 @@ const linkProblems = ({ props }) => {
|
|
|
33
34
|
};
|
|
34
35
|
/**
|
|
35
36
|
* Checks a page's tree without the models: the root is one `Page`, every built-in element has known props of the right
|
|
36
|
-
* kind, elements that show a record's fields sit inside one,
|
|
37
|
-
* where it is.
|
|
37
|
+
* kind, elements that show a record's fields sit inside one, tabs are placed as their address needs, and conditions
|
|
38
|
+
* parse. Returns the problems, each with where it is.
|
|
38
39
|
*/
|
|
39
40
|
const structureProblems = (tree) => {
|
|
40
41
|
const problems = [];
|
|
@@ -55,7 +56,7 @@ const structureProblems = (tree) => {
|
|
|
55
56
|
if (!parsed.ok) report(`"when" is not a valid condition: ${parsed.message}`);
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
|
-
return problems;
|
|
59
|
+
return [...problems, ...tabsProblems(tree)];
|
|
59
60
|
};
|
|
60
61
|
//#endregion
|
|
61
62
|
export { structureProblems };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { walkTree } from "./walk.js";
|
|
2
|
+
//#region src/check-tabs.ts
|
|
3
|
+
const tabName = /^[a-z][a-zA-Z0-9-]*$/;
|
|
4
|
+
const isBuiltIn = (child, type) => typeof child !== "string" && !child.custom && child.type === type;
|
|
5
|
+
const ownProblems = (tabs) => {
|
|
6
|
+
const problems = [];
|
|
7
|
+
if (tabs.children.some((child) => !isBuiltIn(child, "Tab"))) problems.push("holds only <Tab> elements");
|
|
8
|
+
const names = tabs.children.filter((child) => isBuiltIn(child, "Tab")).map((tab) => tab.props.name);
|
|
9
|
+
if (names.length === 0) problems.push("holds no <Tab>");
|
|
10
|
+
for (const name of names.filter((name) => typeof name === "string" && !tabName.test(name))) problems.push(`tab name "${String(name)}" must start with a lowercase letter and hold only letters, digits and dashes`);
|
|
11
|
+
for (const name of new Set(names.filter((name, index) => names.indexOf(name) !== index))) problems.push(`two tabs are named "${String(name)}"`);
|
|
12
|
+
return problems;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Problems with a page's tabs: the tabs take the page's address, so a page holds at most one `Tabs`, directly in its
|
|
16
|
+
* `Page`, each `Tab` sits directly in it, and every tab has a name of its own that fits in an address.
|
|
17
|
+
*/
|
|
18
|
+
const tabsProblems = (tree) => {
|
|
19
|
+
const problems = [];
|
|
20
|
+
const placed = /* @__PURE__ */ new Set();
|
|
21
|
+
let count = 0;
|
|
22
|
+
walkTree(tree, (node, { path }) => {
|
|
23
|
+
if (node.custom) return;
|
|
24
|
+
if (node.type === "Tabs") {
|
|
25
|
+
count += 1;
|
|
26
|
+
if (!tree.children.includes(node)) problems.push(`${path}: <Tabs> only sits directly in the <Page>`);
|
|
27
|
+
if (count === 2) problems.push(`${path}: a page holds one <Tabs>, as its tabs take the page's address`);
|
|
28
|
+
for (const child of node.children) if (isBuiltIn(child, "Tab")) placed.add(child);
|
|
29
|
+
ownProblems(node).forEach((problem) => problems.push(`${path}: ${problem}`));
|
|
30
|
+
}
|
|
31
|
+
if (node.type === "Tab" && !placed.has(node)) problems.push(`${path}: <Tab> only sits directly in a <Tabs>`);
|
|
32
|
+
});
|
|
33
|
+
return problems;
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { tabsProblems };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Block, LayoutChild, LayoutChildren, LayoutNode, LayoutValue, isLayoutNode } from "./node.js";
|
|
2
|
-
import { Action, ActionProps, ButtonVariant, Card, CardProps, CardRow, CardRowProps, CardWidth, CardsLayout, Chart, ChartBucket, ChartProps, ChartType, DataProps, Elements, Field, FieldProps, Grid, GridProps, GridSpan, Link, LinkProps, ModalForm, ModalFormProps, Page, PageProps, Progress, ProgressProps, RecordCard, RecordCardProps, RecordCards, RecordCardsProps, RecordRow, RecordRowProps, Show, ShowProps, Stat, StatProps, Table, TableProps, Users, UsersProps } from "./blocks.js";
|
|
2
|
+
import { AccountBlockProps, Action, ActionProps, ButtonVariant, Card, CardProps, CardRow, CardRowProps, CardWidth, CardsLayout, Chart, ChartBucket, ChartProps, ChartType, ConnectedAccounts, ConnectedApps, DataProps, Devices, Elements, Field, FieldProps, Grid, GridProps, GridSpan, Link, LinkProps, ModalForm, ModalFormProps, Page, PageProps, Passkeys, Password, Profile, Progress, ProgressProps, RecordCard, RecordCardProps, RecordCards, RecordCardsProps, RecordRow, RecordRowProps, Settings, SettingsProps, Show, ShowProps, Stat, StatProps, Tab, TabProps, Table, TableProps, Tabs, TabsProps, TwoFactor, Users, UsersProps } from "./blocks.js";
|
|
3
3
|
import { ComponentOptions, component } from "./component.js";
|
|
4
|
-
import {
|
|
4
|
+
import { accountBlocks, accountPageName } from "./check-account.js";
|
|
5
5
|
import { PageDefinition, PageModel, PageOptions, isPageDefinition, page } from "./page.js";
|
|
6
|
+
import { settingsFields, settingsResources, withSettingsFields } from "./settings.js";
|
|
7
|
+
import { accountPage, accountTabs } from "./presets/account-page.js";
|
|
8
|
+
import { usersPage } from "./presets/users-page.js";
|
|
6
9
|
import { cardsLayoutOf } from "./cards-layout.js";
|
|
7
|
-
import { Location, walkTree } from "./walk.js";
|
|
10
|
+
import { Location, mapTree, walkTree } from "./walk.js";
|
|
8
11
|
import { checkPages } from "./check-page.js";
|
|
9
12
|
import { conditionProblems, conditionReadable } from "./check-condition.js";
|
|
10
13
|
import { prunePage } from "./prune-page.js";
|
|
@@ -12,4 +15,4 @@ import { countedResource, evaluateCondition, parseCondition } from "./condition.
|
|
|
12
15
|
import { PathStep, resolveFieldPath } from "./field-path.js";
|
|
13
16
|
import { jsx_runtime_d_exports } from "./jsx-runtime.js";
|
|
14
17
|
import { FieldRefValue, fieldRef } from "@protobase/schema";
|
|
15
|
-
export { Action, type ActionProps, type Block, type ButtonVariant, Card, type CardProps, CardRow, type CardRowProps, type CardWidth, type CardsLayout, 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, RecordCards, type RecordCardsProps, RecordRow, type RecordRowProps, Show, type ShowProps, Stat, type StatProps, Table, type TableProps, Users, type UsersProps, cardsLayoutOf, checkPages, component, conditionProblems, conditionReadable, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_d_exports as jsxRuntime, page, parseCondition, prunePage, resolveFieldPath, usersPage, walkTree };
|
|
18
|
+
export { type AccountBlockProps, Action, type ActionProps, type Block, type ButtonVariant, Card, type CardProps, CardRow, type CardRowProps, type CardWidth, type CardsLayout, Chart, type ChartBucket, type ChartProps, type ChartType, type ComponentOptions, ConnectedAccounts, ConnectedApps, type DataProps, Devices, 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, Passkeys, Password, type PathStep, Profile, Progress, type ProgressProps, RecordCard, type RecordCardProps, RecordCards, type RecordCardsProps, RecordRow, type RecordRowProps, Settings, type SettingsProps, Show, type ShowProps, Stat, type StatProps, Tab, type TabProps, Table, type TableProps, Tabs, type TabsProps, TwoFactor, Users, type UsersProps, accountBlocks, accountPage, accountPageName, accountTabs, cardsLayoutOf, checkPages, component, conditionProblems, conditionReadable, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_d_exports as jsxRuntime, mapTree, page, parseCondition, prunePage, resolveFieldPath, settingsFields, settingsResources, usersPage, walkTree, withSettingsFields };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { isLayoutNode } from "./node.js";
|
|
2
|
-
import { Action, Card, CardRow, Chart, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordCards, RecordRow, Show, Stat, Table, Users } from "./blocks.js";
|
|
2
|
+
import { Action, Card, CardRow, Chart, ConnectedAccounts, ConnectedApps, Devices, Field, Grid, Link, ModalForm, Page, Passkeys, Password, Profile, Progress, RecordCard, RecordCards, RecordRow, Settings, Show, Stat, Tab, Table, Tabs, TwoFactor, Users } from "./blocks.js";
|
|
3
3
|
import { component } from "./component.js";
|
|
4
|
+
import { mapTree, walkTree } from "./walk.js";
|
|
5
|
+
import { accountBlocks, accountPageName } from "./check-account.js";
|
|
6
|
+
import { settingsFields, settingsResources, withSettingsFields } from "./settings.js";
|
|
4
7
|
import { countedResource, evaluateCondition, parseCondition } from "./condition.js";
|
|
5
|
-
import { walkTree } from "./walk.js";
|
|
6
8
|
import { isPageDefinition, page } from "./page.js";
|
|
9
|
+
import { accountPage, accountTabs } from "./presets/account-page.js";
|
|
7
10
|
import { usersPage } from "./presets/users-page.js";
|
|
8
11
|
import { cardsLayoutOf } from "./cards-layout.js";
|
|
9
12
|
import { resolveFieldPath } from "./field-path.js";
|
|
@@ -12,4 +15,4 @@ import { checkPages } from "./check-page.js";
|
|
|
12
15
|
import { prunePage } from "./prune-page.js";
|
|
13
16
|
import { jsx_runtime_exports } from "./jsx-runtime.js";
|
|
14
17
|
import { fieldRef } from "@protobase/schema";
|
|
15
|
-
export { Action, Card, CardRow, Chart, Field, Grid, Link, ModalForm, Page, Progress, RecordCard, RecordCards, RecordRow, Show, Stat, Table, Users, cardsLayoutOf, checkPages, component, conditionProblems, conditionReadable, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_exports as jsxRuntime, page, parseCondition, prunePage, resolveFieldPath, usersPage, walkTree };
|
|
18
|
+
export { Action, Card, CardRow, Chart, ConnectedAccounts, ConnectedApps, Devices, Field, Grid, Link, ModalForm, Page, Passkeys, Password, Profile, Progress, RecordCard, RecordCards, RecordRow, Settings, Show, Stat, Tab, Table, Tabs, TwoFactor, Users, accountBlocks, accountPage, accountPageName, accountTabs, cardsLayoutOf, checkPages, component, conditionProblems, conditionReadable, countedResource, evaluateCondition, fieldRef, isLayoutNode, isPageDefinition, jsx_runtime_exports as jsxRuntime, mapTree, page, parseCondition, prunePage, resolveFieldPath, settingsFields, settingsResources, usersPage, walkTree, withSettingsFields };
|
package/dist/page.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { accountBlockProblems } from "./check-account.js";
|
|
1
2
|
import { structureProblems } from "./check-structure.js";
|
|
2
3
|
//#region src/page.ts
|
|
3
4
|
const pageName = /^[a-z][a-zA-Z0-9-]*$/;
|
|
@@ -7,7 +8,7 @@ const pageName = /^[a-z][a-zA-Z0-9-]*$/;
|
|
|
7
8
|
*/
|
|
8
9
|
const page = (name, tree, options = {}) => {
|
|
9
10
|
if (!pageName.test(name)) throw new Error(`Page name "${name}" must start with a lowercase letter and hold only letters, digits and dashes`);
|
|
10
|
-
const problems = structureProblems(tree);
|
|
11
|
+
const problems = [...structureProblems(tree), ...accountBlockProblems(name, tree)];
|
|
11
12
|
if (problems.length > 0) throw new Error(`Page "${name}" is not a valid layout:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
12
13
|
const model = {
|
|
13
14
|
name,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LayoutNode } from "../node.js";
|
|
2
|
+
import { PageDefinition } from "../page.js";
|
|
3
|
+
import "../index.js";
|
|
4
|
+
//#region src/presets/account-page.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The standard tabs of the account page, by name, for an account page of the app's own: list the ones to keep, in
|
|
7
|
+
* the order to show them, beside tabs of its own. Written as calls rather than JSX, as `usersPage` is.
|
|
8
|
+
*/
|
|
9
|
+
export declare const accountTabs: {
|
|
10
|
+
profile: LayoutNode;
|
|
11
|
+
security: LayoutNode;
|
|
12
|
+
devices: LayoutNode;
|
|
13
|
+
apps: LayoutNode;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The account page everyone signed in has at `/-/account`, each tab below it (`/-/account/security`). The server
|
|
17
|
+
* serves it when the config exports no page named `account`; exporting one replaces it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const accountPage: PageDefinition;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ConnectedAccounts, ConnectedApps, Devices, Page, Passkeys, Password, Profile, Tab, Tabs, TwoFactor } from "../blocks.js";
|
|
2
|
+
import { accountPageName } from "../check-account.js";
|
|
3
|
+
import { page } from "../page.js";
|
|
4
|
+
//#region src/presets/account-page.ts
|
|
5
|
+
/**
|
|
6
|
+
* The standard tabs of the account page, by name, for an account page of the app's own: list the ones to keep, in
|
|
7
|
+
* the order to show them, beside tabs of its own. Written as calls rather than JSX, as `usersPage` is.
|
|
8
|
+
*/
|
|
9
|
+
const accountTabs = {
|
|
10
|
+
profile: Tab({
|
|
11
|
+
name: "profile",
|
|
12
|
+
label: "Profile",
|
|
13
|
+
children: [Profile({})]
|
|
14
|
+
}),
|
|
15
|
+
security: Tab({
|
|
16
|
+
name: "security",
|
|
17
|
+
label: "Sign-in & security",
|
|
18
|
+
children: [
|
|
19
|
+
Password({}),
|
|
20
|
+
Passkeys({}),
|
|
21
|
+
TwoFactor({}),
|
|
22
|
+
ConnectedAccounts({})
|
|
23
|
+
]
|
|
24
|
+
}),
|
|
25
|
+
devices: Tab({
|
|
26
|
+
name: "devices",
|
|
27
|
+
label: "Devices",
|
|
28
|
+
children: [Devices({})]
|
|
29
|
+
}),
|
|
30
|
+
apps: Tab({
|
|
31
|
+
name: "apps",
|
|
32
|
+
label: "Connected apps",
|
|
33
|
+
children: [ConnectedApps({})]
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The account page everyone signed in has at `/-/account`, each tab below it (`/-/account/security`). The server
|
|
38
|
+
* serves it when the config exports no page named `account`; exporting one replaces it.
|
|
39
|
+
*/
|
|
40
|
+
const accountPage = page(accountPageName, Page({
|
|
41
|
+
title: "Account",
|
|
42
|
+
children: Tabs({ children: Object.values(accountTabs) })
|
|
43
|
+
}));
|
|
44
|
+
//#endregion
|
|
45
|
+
export { accountPage, accountTabs };
|
package/dist/prune-page.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ export type CallerAccess = {
|
|
|
16
16
|
/**
|
|
17
17
|
* The page as one caller may see it, or `undefined` when its roles leave the caller out. Every element that names a
|
|
18
18
|
* resource, field, filter, sort or action the caller cannot use is left out, with what it contains, custom components
|
|
19
|
-
* by their `resource` and field references (`fieldRef`) too
|
|
20
|
-
* caller may read and write. Data still comes from the API, which applies
|
|
21
|
-
*
|
|
19
|
+
* by their `resource` and field references (`fieldRef`) too, and so is a tab left with nothing in it; table columns and
|
|
20
|
+
* form fields are narrowed to what the caller may read and write. Data still comes from the API, which applies the same
|
|
21
|
+
* access rules (row filters included) to every request.
|
|
22
22
|
*/
|
|
23
23
|
export declare const prunePage: (page: PageModel, roles: readonly string[], access: CallerAccess) => PageModel | undefined;
|
|
24
24
|
//#endregion
|
package/dist/prune-page.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dataProps } from "./node.js";
|
|
2
|
-
import { countedResource, parseCondition } from "./condition.js";
|
|
3
2
|
import { mapTree } from "./walk.js";
|
|
3
|
+
import { countedResource, parseCondition } from "./condition.js";
|
|
4
4
|
import { chartFields } from "./check-chart.js";
|
|
5
5
|
import { resolveFieldPath } from "./field-path.js";
|
|
6
6
|
import { elementResource, linkSource } from "./check-page.js";
|
|
@@ -80,6 +80,17 @@ const prune = (node, scope, access) => {
|
|
|
80
80
|
}
|
|
81
81
|
} : null;
|
|
82
82
|
}
|
|
83
|
+
case "Settings": {
|
|
84
|
+
if (!model) return null;
|
|
85
|
+
const fields = (node.props.fields ?? []).filter((name) => Object.hasOwn(model.fields, name));
|
|
86
|
+
return fields.length > 0 ? {
|
|
87
|
+
...node,
|
|
88
|
+
props: {
|
|
89
|
+
...node.props,
|
|
90
|
+
fields
|
|
91
|
+
}
|
|
92
|
+
} : null;
|
|
93
|
+
}
|
|
83
94
|
case "Action": return resource !== void 0 && model && access.actions(resource).has(String(node.props.name)) ? node : null;
|
|
84
95
|
case "Link": return node.props.to === void 0 || addressErrors(node.props.to, {
|
|
85
96
|
models,
|
|
@@ -95,15 +106,17 @@ const prune = (node, scope, access) => {
|
|
|
95
106
|
return counted !== void 0 ? Boolean(models[counted]) : readable(name);
|
|
96
107
|
}) ? node : null;
|
|
97
108
|
}
|
|
109
|
+
case "Tab":
|
|
110
|
+
case "Tabs": return node.children.length > 0 ? node : null;
|
|
98
111
|
default: return node;
|
|
99
112
|
}
|
|
100
113
|
};
|
|
101
114
|
/**
|
|
102
115
|
* The page as one caller may see it, or `undefined` when its roles leave the caller out. Every element that names a
|
|
103
116
|
* resource, field, filter, sort or action the caller cannot use is left out, with what it contains, custom components
|
|
104
|
-
* by their `resource` and field references (`fieldRef`) too
|
|
105
|
-
* caller may read and write. Data still comes from the API, which applies
|
|
106
|
-
*
|
|
117
|
+
* by their `resource` and field references (`fieldRef`) too, and so is a tab left with nothing in it; table columns and
|
|
118
|
+
* form fields are narrowed to what the caller may read and write. Data still comes from the API, which applies the same
|
|
119
|
+
* access rules (row filters included) to every request.
|
|
107
120
|
*/
|
|
108
121
|
const prunePage = (page, roles, access) => {
|
|
109
122
|
if (page.roles && !page.roles.some((role) => roles.includes(role))) return void 0;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PageModel } from "./page.js";
|
|
2
|
+
import { ResourceModel } from "@protobase/schema";
|
|
3
|
+
//#region src/settings.d.ts
|
|
4
|
+
/** The fields a `Settings` of `model` shows without `fields`: every one a person can set, in the model's order. */
|
|
5
|
+
export declare const settingsFields: (model: ResourceModel) => string[];
|
|
6
|
+
/** The resources the pages' `Settings` blocks show, which every caller reads and writes only their own records of. */
|
|
7
|
+
export declare const settingsResources: (pages: readonly PageModel[]) => Set<string>;
|
|
8
|
+
/** The page with each `Settings` without `fields` given its default fields, from the full models, before pruning. */
|
|
9
|
+
export declare const withSettingsFields: (page: PageModel, models: Record<string, ResourceModel>) => PageModel;
|
|
10
|
+
//#endregion
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { mapTree, walkTree } from "./walk.js";
|
|
2
|
+
//#region src/settings.ts
|
|
3
|
+
const setBySystem = (model) => /* @__PURE__ */ new Set([
|
|
4
|
+
...model.primaryKey,
|
|
5
|
+
model.owner,
|
|
6
|
+
model.tenant,
|
|
7
|
+
model.softDelete,
|
|
8
|
+
model.position?.field
|
|
9
|
+
]);
|
|
10
|
+
/** The fields a `Settings` of `model` shows without `fields`: every one a person can set, in the model's order. */
|
|
11
|
+
const settingsFields = (model) => {
|
|
12
|
+
const system = setBySystem(model);
|
|
13
|
+
return Object.entries(model.fields).filter(([name, field]) => !system.has(name) && !field.readOnly && !field.sensitive && !field.file).map(([name]) => name);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* What keeps `model` from holding settings: an owner that is its whole key, so each person has one record, and no
|
|
17
|
+
* tenant, as settings are personal; and the `fields` it names.
|
|
18
|
+
*/
|
|
19
|
+
const settingsProblems = (model, fields) => {
|
|
20
|
+
const { name, owner, primaryKey, tenant } = model;
|
|
21
|
+
if (owner === void 0) return [`${name} has no .owner(...), so its records are not each person's own`];
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (primaryKey.length !== 1 || primaryKey[0] !== owner) problems.push(`the owner "${owner}" must be the key of ${name}, so each person has one record: .primaryKey((r) => r.${owner})`);
|
|
24
|
+
if (tenant !== void 0) problems.push(`${name} has a tenant, and settings are each person's own: leave out .tenant()`);
|
|
25
|
+
if (fields === void 0) {
|
|
26
|
+
if (settingsFields(model).length === 0) problems.push(`${name} has no field a person can set`);
|
|
27
|
+
return problems;
|
|
28
|
+
}
|
|
29
|
+
const system = setBySystem(model);
|
|
30
|
+
for (const field of fields) {
|
|
31
|
+
const found = Object.hasOwn(model.fields, field) ? model.fields[field] : void 0;
|
|
32
|
+
if (!found) problems.push(`unknown field "${field}" on ${name}`);
|
|
33
|
+
else if (system.has(field)) problems.push(`"${field}" is set by the server on ${name}, so it is no setting`);
|
|
34
|
+
else if (found.sensitive) problems.push(`"${field}" is sensitive, and Settings does not show sensitive fields`);
|
|
35
|
+
else if (found.file) problems.push(`"${field}" is a file, and Settings does not upload files`);
|
|
36
|
+
}
|
|
37
|
+
return problems;
|
|
38
|
+
};
|
|
39
|
+
const isSettings = (node) => !node.custom && node.type === "Settings" && typeof node.props.resource === "string";
|
|
40
|
+
/** The resources the pages' `Settings` blocks show, which every caller reads and writes only their own records of. */
|
|
41
|
+
const settingsResources = (pages) => {
|
|
42
|
+
const found = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const page of pages) walkTree(page.tree, (node) => void (isSettings(node) && found.add(String(node.props.resource))));
|
|
44
|
+
return found;
|
|
45
|
+
};
|
|
46
|
+
/** The page with each `Settings` without `fields` given its default fields, from the full models, before pruning. */
|
|
47
|
+
const withSettingsFields = (page, models) => {
|
|
48
|
+
const tree = mapTree(page.tree, (node) => {
|
|
49
|
+
if (!isSettings(node) || node.props.fields !== void 0) return node;
|
|
50
|
+
const model = models[String(node.props.resource)];
|
|
51
|
+
return model ? {
|
|
52
|
+
...node,
|
|
53
|
+
props: {
|
|
54
|
+
...node.props,
|
|
55
|
+
fields: settingsFields(model)
|
|
56
|
+
}
|
|
57
|
+
} : node;
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
...page,
|
|
61
|
+
tree
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
export { settingsFields, settingsProblems, settingsResources, withSettingsFields };
|
package/dist/walk.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export type Location = {
|
|
|
9
9
|
path: string;
|
|
10
10
|
scope: Scope;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Rebuilds the tree bottom-up through `visit`, which sees each element (with its children and element props already
|
|
14
|
+
* rebuilt) and returns it, a replacement, or `null` to leave it out. Text children are kept as they are.
|
|
15
|
+
*/
|
|
16
|
+
export declare const mapTree: (node: LayoutNode, visit: (node: LayoutNode, at: Location) => LayoutNode | null, at?: Location) => LayoutNode | null;
|
|
12
17
|
/** Calls `visit` for every element, parents first. */
|
|
13
18
|
export declare const walkTree: (node: LayoutNode, visit: (node: LayoutNode, at: Location) => void, at?: Location) => void;
|
|
14
19
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/layout",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.4",
|
|
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.46.
|
|
34
|
+
"@protobase/schema": "0.46.4",
|
|
35
35
|
"aip-parsers": "^1.0.0"
|
|
36
36
|
}
|
|
37
37
|
}
|