@protobase/ui 0.21.9 → 0.21.11
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/account/account-security-page.js +5 -4
- package/dist/app/create/create-values.js +2 -1
- package/dist/app/pages/blocks/show.js +5 -34
- package/dist/app/pages/use-conditions.js +49 -0
- package/dist/app/record-mode/record-mode.js +4 -1
- package/dist/app/record-mode/shown-layout.js +18 -0
- package/dist/app/record-mode/use-shown-layout.js +14 -0
- package/dist/app/record-page.js +9 -8
- package/dist/app/related-section.js +9 -0
- package/dist/auth/auth-messages.js +2 -1
- package/dist/auth/auth-provider.js +8 -3
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-CiQapuHq.js +69 -0
- package/dist/shell/assets/{index-CRNWPM01.js → index-Dimu63Ym.js} +1 -1
- package/dist/shell/assets/{maplibre-DZlOrjmh.js → maplibre-ERswanhA.js} +1 -1
- package/dist/shell/assets/{markdown-content-DT_18T89.js → markdown-content-D-haiVrI.js} +1 -1
- package/dist/shell/assets/{record-map-Bmns3gJf.js → record-map-C3i03kwc.js} +1 -1
- package/dist/shell/assets/{records-map-Ci3qFCDs.js → records-map-C1JyIuz3.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DPgVYrS4.js → shared-protobase-ui-Bt4hh6GB.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-B_L-OYQJ.js +0 -69
|
@@ -17,7 +17,8 @@ const takeLinkError = () => {
|
|
|
17
17
|
};
|
|
18
18
|
/** The signed-in person's passkeys, two-factor authentication and connected accounts, within what the sign-in policy allows. */
|
|
19
19
|
const AccountSecurityPage = ({ email }) => {
|
|
20
|
-
const { session, signInProviders, linkProvider } = useAuth();
|
|
20
|
+
const { session, signInProviders, socialProviders, linkProvider } = useAuth();
|
|
21
|
+
const linkable = signInProviders.filter((provider) => socialProviders.includes(provider.id));
|
|
21
22
|
const { account } = session;
|
|
22
23
|
const [loaded, setLoaded] = useState();
|
|
23
24
|
const [error, setError] = useState();
|
|
@@ -61,11 +62,11 @@ const AccountSecurityPage = ({ email }) => {
|
|
|
61
62
|
onNewBackupCodes: (password) => account.newBackupCodes(password),
|
|
62
63
|
onChanged: reload
|
|
63
64
|
}),
|
|
64
|
-
|
|
65
|
-
providers:
|
|
65
|
+
linkable.length > 0 && /* @__PURE__ */ jsx(SignInProvidersSection, {
|
|
66
|
+
providers: linkable,
|
|
66
67
|
linked: loaded.linked,
|
|
67
68
|
onConnect: linkProvider,
|
|
68
|
-
error: linkError && providerMessage(linkError,
|
|
69
|
+
error: linkError && providerMessage(linkError, linkable.length === 1 ? linkable[0].name : "the provider")
|
|
69
70
|
})
|
|
70
71
|
] }) : error ? /* @__PURE__ */ jsx("p", {
|
|
71
72
|
role: "alert",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isEditable, toPatchValue } from "../../live/field-values.js";
|
|
2
2
|
import { whenShown } from "../record-mode/record-mode.js";
|
|
3
|
+
import { isRecordCondition } from "@protobase/schema";
|
|
3
4
|
//#region src/app/create/create-values.ts
|
|
4
5
|
const isChosen = (value) => typeof value === "object" && value !== null && "key" in value;
|
|
5
6
|
/** Fields the user can set on a new record: not read-only, not the tenant, and not structured data. */
|
|
@@ -13,7 +14,7 @@ const createSections = (model, view) => {
|
|
|
13
14
|
const creatable = Object.values(model.fields).filter((field) => isCreatable(model, field)).map((field) => field.name);
|
|
14
15
|
const placed = /* @__PURE__ */ new Set();
|
|
15
16
|
const sections = (view?.layout ?? []).flatMap((item) => {
|
|
16
|
-
if (item.kind !== "section" || !whenShown(item.when, true)) return [];
|
|
17
|
+
if (item.kind !== "section" || isRecordCondition(item.when) || !whenShown(item.when, true)) return [];
|
|
17
18
|
const fields = item.fields.filter((name) => creatable.includes(name) && !placed.has(name) && whenShown(view?.fields[name]?.when, true));
|
|
18
19
|
fields.forEach((name) => placed.add(name));
|
|
19
20
|
return fields.length > 0 ? [{
|
|
@@ -1,43 +1,14 @@
|
|
|
1
|
-
import { useClient } from "../../../data/api-provider.js";
|
|
2
|
-
import { useAdminMeta } from "../../meta-gate.js";
|
|
3
1
|
import { useLayoutRecord } from "../record-scope.js";
|
|
4
|
-
import { countQuery } from "../use-layout-data.js";
|
|
5
2
|
import { propsOf } from "./block-props.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import { useQueries } from "@tanstack/react-query";
|
|
10
|
-
import { countedResource, evaluateCondition, parseCondition } from "@protobase/layout";
|
|
3
|
+
import { useConditions } from "../use-conditions.js";
|
|
4
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
5
|
+
import "@protobase/layout";
|
|
11
6
|
//#region src/app/pages/blocks/show.tsx
|
|
12
7
|
/** Its children while `when` holds: fields of the record around it, and `<resource>.count` for any resource. */
|
|
13
8
|
const ShowBlock = ({ node, children }) => {
|
|
14
9
|
const { when } = propsOf(node);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const scope = useLayoutRecord();
|
|
18
|
-
const condition = useMemo(() => parseCondition(when), [when]);
|
|
19
|
-
const paths = condition.ok ? condition.paths : [];
|
|
20
|
-
const counted = paths.map((path) => countedResource(path, (name) => Boolean(scope && Object.hasOwn(scope.model.fields, name))));
|
|
21
|
-
const fieldPaths = paths.filter((_, index) => counted[index] === void 0);
|
|
22
|
-
const fields = usePathValues(scope, fieldPaths);
|
|
23
|
-
const counts = useQueries({ queries: counted.flatMap((resource) => {
|
|
24
|
-
const model = resource === void 0 ? void 0 : resources[resource];
|
|
25
|
-
if (!model) return [];
|
|
26
|
-
return [countQuery(client, model)];
|
|
27
|
-
}) });
|
|
28
|
-
if (!condition.ok) return null;
|
|
29
|
-
if (fields.some((value) => value.state === "loading") || counts.some((count) => count.isPending)) return null;
|
|
30
|
-
const values = {};
|
|
31
|
-
fieldPaths.forEach((path, index) => {
|
|
32
|
-
const value = fields[index];
|
|
33
|
-
if (value.state === "ready") values[path] = value.value;
|
|
34
|
-
});
|
|
35
|
-
let next = 0;
|
|
36
|
-
paths.forEach((path, index) => {
|
|
37
|
-
if (counted[index] === void 0) return;
|
|
38
|
-
values[path] = resources[counted[index]] ? counts[next++]?.data ?? 0 : 0;
|
|
39
|
-
});
|
|
40
|
-
return evaluateCondition(condition, values) ? /* @__PURE__ */ jsx(Fragment$1, { children }) : null;
|
|
10
|
+
const [shown] = useConditions([when], useLayoutRecord());
|
|
11
|
+
return shown ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
|
|
41
12
|
};
|
|
42
13
|
//#endregion
|
|
43
14
|
export { ShowBlock };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useClient } from "../../data/api-provider.js";
|
|
2
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
3
|
+
import { countQuery } from "./use-layout-data.js";
|
|
4
|
+
import { usePathValues } from "./use-path-value.js";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { useQueries } from "@tanstack/react-query";
|
|
7
|
+
import { countedResource, evaluateCondition, parseCondition } from "@protobase/layout";
|
|
8
|
+
//#region src/app/pages/use-conditions.ts
|
|
9
|
+
/**
|
|
10
|
+
* Whether each condition holds for the record around it (`scope`): `undefined` while a value it reads loads, `false`
|
|
11
|
+
* when it does not parse. Fields come from the stored record, followed through relations; `<resource>.count` counts
|
|
12
|
+
* records. All conditions share one set of queries, so the hook count never changes with the conditions.
|
|
13
|
+
*/
|
|
14
|
+
const useConditions = (whens, scope) => {
|
|
15
|
+
const { resources } = useAdminMeta();
|
|
16
|
+
const client = useClient();
|
|
17
|
+
const key = JSON.stringify(whens);
|
|
18
|
+
const parsed = useMemo(() => JSON.parse(key).map(parseCondition), [key]);
|
|
19
|
+
const isField = (name) => Boolean(scope && Object.hasOwn(scope.model.fields, name));
|
|
20
|
+
const paths = [...new Set(parsed.flatMap((condition) => condition.ok ? condition.paths : []))];
|
|
21
|
+
const counted = new Map(paths.flatMap((path) => {
|
|
22
|
+
const resource = countedResource(path, isField);
|
|
23
|
+
return resource === void 0 ? [] : [[path, resource]];
|
|
24
|
+
}));
|
|
25
|
+
const fieldPaths = paths.filter((path) => !counted.has(path));
|
|
26
|
+
const countedModels = [...new Set(counted.values())].flatMap((resource) => resources[resource] ? [resources[resource]] : []);
|
|
27
|
+
const fields = usePathValues(scope, fieldPaths);
|
|
28
|
+
const counts = useQueries({ queries: countedModels.map((model) => countQuery(client, model)) });
|
|
29
|
+
return parsed.map((condition) => {
|
|
30
|
+
if (!condition.ok) return false;
|
|
31
|
+
const values = {};
|
|
32
|
+
for (const path of condition.paths) {
|
|
33
|
+
const resource = counted.get(path);
|
|
34
|
+
if (resource === void 0) {
|
|
35
|
+
const value = fields[fieldPaths.indexOf(path)];
|
|
36
|
+
if (value.state === "loading") return void 0;
|
|
37
|
+
if (value.state === "ready") values[path] = value.value;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const index = countedModels.findIndex((model) => model.name === resource);
|
|
41
|
+
if (index === -1) values[path] = 0;
|
|
42
|
+
else if (counts[index].isPending) return void 0;
|
|
43
|
+
else values[path] = counts[index].data ?? 0;
|
|
44
|
+
}
|
|
45
|
+
return evaluateCondition(condition, values);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { useConditions };
|
|
@@ -32,7 +32,10 @@ const recordShown = (mode, canUpdate, edit) => {
|
|
|
32
32
|
const helpShown = (own, view, editing) => (own ?? view?.helpWhen ?? "always") === "always" || editing;
|
|
33
33
|
/** A field's help line on the record page, when it shows. */
|
|
34
34
|
const fieldHelp = (hints, view, editing) => hints?.help !== void 0 && helpShown(hints.helpWhen, view, editing) ? hints.help : void 0;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Whether a field or layout item shows: always, unless it says only while editing or only while reading and the page is not.
|
|
37
|
+
* A record condition is no mode and counts as always here; the record page evaluates it separately.
|
|
38
|
+
*/
|
|
36
39
|
const whenShown = (when, editing) => {
|
|
37
40
|
if (when === "editing") return editing;
|
|
38
41
|
if (when === "reading") return !editing;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { whenShown } from "./record-mode.js";
|
|
2
|
+
import { isRecordCondition } from "@protobase/schema";
|
|
3
|
+
//#region src/app/record-mode/shown-layout.ts
|
|
4
|
+
/** The items with a record condition in `when`, whose conditions the page evaluates. */
|
|
5
|
+
const conditionalItems = (items) => items.filter((item) => isRecordCondition(item.when));
|
|
6
|
+
/**
|
|
7
|
+
* The items that show: those with a record condition only when it holds (`held`, one answer per conditional item in
|
|
8
|
+
* order, `undefined` while loading) and the page is no preview; the others by the form or read view.
|
|
9
|
+
*/
|
|
10
|
+
const shownLayout = (items, held, editing, preview) => {
|
|
11
|
+
let next = 0;
|
|
12
|
+
return items.filter((item) => {
|
|
13
|
+
if (!isRecordCondition(item.when)) return whenShown(item.when, editing);
|
|
14
|
+
return !preview && held[next++] === true;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { conditionalItems, shownLayout };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useConditions } from "../pages/use-conditions.js";
|
|
2
|
+
import { conditionalItems, shownLayout } from "./shown-layout.js";
|
|
3
|
+
//#region src/app/record-mode/use-shown-layout.ts
|
|
4
|
+
/**
|
|
5
|
+
* The layout items to draw: those meant for the form or the read view as the page is, and those with a record
|
|
6
|
+
* condition while it holds for the saved record. Conditional items stay out while their values load and on a preview.
|
|
7
|
+
*/
|
|
8
|
+
const useShownLayout = (items, scope, editing, preview) => {
|
|
9
|
+
const conditions = preview ? [] : conditionalItems(items).map((item) => item.when);
|
|
10
|
+
const held = useConditions(conditions, scope);
|
|
11
|
+
return shownLayout(items, held, editing, preview);
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { useShownLayout };
|
package/dist/app/record-page.js
CHANGED
|
@@ -52,6 +52,7 @@ import { useLinesDraft } from "./use-lines-draft.js";
|
|
|
52
52
|
import { GallerySection } from "./gallery/gallery-section.js";
|
|
53
53
|
import { HistorySection } from "./history/history-section.js";
|
|
54
54
|
import { CommentsSection } from "./comments/comments-section.js";
|
|
55
|
+
import { useShownLayout } from "./record-mode/use-shown-layout.js";
|
|
55
56
|
import { useRecordEditor } from "./use-record-editor.js";
|
|
56
57
|
import { useRelatedRecords } from "./use-related-records.js";
|
|
57
58
|
import { IncomingCard } from "./incoming-card.js";
|
|
@@ -190,7 +191,14 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
190
191
|
editor.change(name, keyValue(target, next.key));
|
|
191
192
|
};
|
|
192
193
|
const { fields: fieldTypes } = useProjectUi();
|
|
193
|
-
const
|
|
194
|
+
const scope = {
|
|
195
|
+
model,
|
|
196
|
+
record: stored.record,
|
|
197
|
+
key: recordKey,
|
|
198
|
+
etag: stored.etag,
|
|
199
|
+
permissions: stored.permissions
|
|
200
|
+
};
|
|
201
|
+
const layout = useShownLayout(layoutFor(model, view, shown === "read"), scope, editing, preview);
|
|
194
202
|
const sidebarFields = layout.flatMap((item) => item.kind === "sidebar" ? item.fields : []);
|
|
195
203
|
const hasStatus = model.fields.status?.type === "enum";
|
|
196
204
|
const statusValue = stored.record.status;
|
|
@@ -307,13 +315,6 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
307
315
|
}];
|
|
308
316
|
})
|
|
309
317
|
});
|
|
310
|
-
const scope = {
|
|
311
|
-
model,
|
|
312
|
-
record: stored.record,
|
|
313
|
-
key: recordKey,
|
|
314
|
-
etag: stored.etag,
|
|
315
|
-
permissions: stored.permissions
|
|
316
|
-
};
|
|
317
318
|
const save = async () => {
|
|
318
319
|
if (await editor.save() && leavesForm) go(readAddress, { replace: true });
|
|
319
320
|
};
|
|
@@ -5,6 +5,7 @@ import { Section } from "../record-view/section.js";
|
|
|
5
5
|
import { ErrorBanner } from "./error-banner.js";
|
|
6
6
|
import { listColumnNames } from "../live/columns-from-model.js";
|
|
7
7
|
import { useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
8
|
+
import { countQuery } from "./pages/use-layout-data.js";
|
|
8
9
|
import { searchAll } from "../live/search-all.js";
|
|
9
10
|
import { relatedOpens } from "./related-opens.js";
|
|
10
11
|
import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
|
|
@@ -82,7 +83,15 @@ const RelatedSection = ({ item, recordKey, owner }) => {
|
|
|
82
83
|
const [display, pick] = useEmbeddedDisplay(view, `related:${owner}:${item.title}`, item.display);
|
|
83
84
|
const permissions = usePermissions(item.resource);
|
|
84
85
|
const [sorting, setSorting] = useState(false);
|
|
86
|
+
const client = useClient();
|
|
87
|
+
const counting = Boolean(item.hideEmpty && model && match.filter);
|
|
88
|
+
const count = useQuery({
|
|
89
|
+
queryKey: keys.count(item.resource, match.filter ?? ""),
|
|
90
|
+
enabled: counting,
|
|
91
|
+
queryFn: () => countQuery(client, model, match.filter ?? "").queryFn()
|
|
92
|
+
});
|
|
85
93
|
if (!model) return null;
|
|
94
|
+
if (item.hideEmpty && (match.isPending || match.filter === null || counting && (count.isPending || count.data === 0))) return null;
|
|
86
95
|
const opens = relatedOpens(item, model, resources);
|
|
87
96
|
const columns = item.columns ?? listColumnNames(model, view).filter((name) => name !== item.field);
|
|
88
97
|
const handOrder = relatedHandOrder(item, model, permissions);
|
|
@@ -49,7 +49,8 @@ const providerMessages = {
|
|
|
49
49
|
account_not_linked: (name) => `Your ${name} account is not linked to your account here. Sign in another way, then connect ${name} on the Sign-in & security page.`,
|
|
50
50
|
account_already_linked_to_different_user: (name) => `This ${name} account is linked to someone else here already.`,
|
|
51
51
|
unable_to_link_account: (name) => `Your ${name} account could not be linked. Try again.`,
|
|
52
|
-
access_denied: (name) => `${name} did not let you in, or you cancelled
|
|
52
|
+
access_denied: (name) => `${name} did not let you in, or you cancelled.`,
|
|
53
|
+
EMAIL_DOMAIN_NOT_ALLOWED: (name) => `${name} signed you in with an address this app does not accept from it. Sign in another way.`
|
|
53
54
|
};
|
|
54
55
|
/** Why signing in or linking with the provider `name` did not go through, from the code it came back with. */
|
|
55
56
|
const providerMessage = (code, name) => providerMessages[code]?.(name) ?? `Signing in with ${name} did not go through. Try again.`;
|
|
@@ -18,6 +18,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
18
18
|
const [passkeyDomains, setPasskeyDomains] = useState([]);
|
|
19
19
|
const [socialProviders, setSocialProviders] = useState([]);
|
|
20
20
|
const [platformSignIn, setPlatformSignIn] = useState();
|
|
21
|
+
const [samlProviders, setSamlProviders] = useState([]);
|
|
21
22
|
const [staffSignIn, setStaffSignIn] = useState();
|
|
22
23
|
const [organizations, setOrganizations] = useState();
|
|
23
24
|
const [anonymous, setAnonymous] = useState(false);
|
|
@@ -52,6 +53,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
52
53
|
setPasskeyDomains(status.passkeyDomains ?? []);
|
|
53
54
|
setSocialProviders(status.socialProviders);
|
|
54
55
|
setPlatformSignIn(status.platformSignIn);
|
|
56
|
+
setSamlProviders(status.samlProviders);
|
|
55
57
|
setStaffSignIn(status.staffSignIn);
|
|
56
58
|
setOrganizations(status.organizations);
|
|
57
59
|
setAnonymous(Boolean(status.anonymous));
|
|
@@ -73,10 +75,10 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
73
75
|
canSignUp,
|
|
74
76
|
passkeyDomains,
|
|
75
77
|
socialProviders,
|
|
76
|
-
signInProviders: socialProviders.map((id) => ({
|
|
78
|
+
signInProviders: [...socialProviders.map((id) => ({
|
|
77
79
|
id,
|
|
78
80
|
name: providerName(id, platformSignIn)
|
|
79
|
-
})),
|
|
81
|
+
})), ...samlProviders],
|
|
80
82
|
staffSignIn,
|
|
81
83
|
...organizations && { organizations },
|
|
82
84
|
markSignedOut,
|
|
@@ -100,7 +102,9 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
100
102
|
signInWithPasskey: async (domain) => enter(await session.signInWithPasskey(domain)),
|
|
101
103
|
verifyTwoFactor: async (input) => enter(await session.verifyTwoFactor(input)),
|
|
102
104
|
signInSocial: async (provider) => {
|
|
103
|
-
|
|
105
|
+
const callbackURL = new URL(withoutSignInLink(window.location.href), window.location.href).href;
|
|
106
|
+
const saml = samlProviders.some((candidate) => candidate.id === provider);
|
|
107
|
+
window.location.assign(await (saml ? session.signInSaml(provider, callbackURL) : session.signInSocial(provider, callbackURL)));
|
|
104
108
|
},
|
|
105
109
|
linkProvider: async (provider) => {
|
|
106
110
|
window.location.assign(await session.account.linkProvider(provider, new URL(withoutSignInLink(window.location.href), window.location.href).href));
|
|
@@ -122,6 +126,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
122
126
|
passkeyDomains,
|
|
123
127
|
socialProviders,
|
|
124
128
|
platformSignIn,
|
|
129
|
+
samlProviders,
|
|
125
130
|
staffSignIn,
|
|
126
131
|
organizations,
|
|
127
132
|
anonymous,
|
package/dist/package.js
CHANGED