@protobase/ui 0.12.8 → 0.12.10
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/action-card/action-card.d.ts +3 -1
- package/dist/action-card/action-card.js +4 -4
- package/dist/app/app.d.ts +4 -1
- package/dist/app/app.js +26 -18
- package/dist/app/create/create-page.js +32 -6
- package/dist/app/create/create-values.js +16 -3
- package/dist/app/current-address.d.ts +1 -0
- package/dist/app/displays/cards-display.js +17 -2
- package/dist/app/displays/list-displays.js +3 -1
- package/dist/app/pages/field-value.js +1 -1
- package/dist/app/pages/use-relation-labels.js +1 -1
- package/dist/app-shell/app-shell.js +2 -1
- package/dist/app-shell/sidebar.js +12 -2
- package/dist/auth/auth-gate.js +17 -11
- package/dist/auth/auth-provider.js +10 -3
- package/dist/auth/sign-in-form.js +7 -1
- package/dist/auth/sign-in-page.js +2 -1
- package/dist/live/use-filter-config.js +1 -1
- package/dist/package.js +1 -1
- package/dist/roles/use-active-role.js +8 -3
- package/dist/shell/assets/app-CUxGSH9m.js +65 -0
- package/dist/shell/assets/index-BIjqw3oN.css +2 -0
- package/dist/shell/assets/{index-YPieq1OK.js → index-CuBoFEnG.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-bPkMP70Y.js → shared-protobase-ui-CR4VI05-.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-BFCjH5PZ.js +0 -65
- package/dist/shell/assets/index-BMIEmNDF.css +0 -2
|
@@ -20,7 +20,9 @@ export type ActionCardProps = {
|
|
|
20
20
|
actions?: CardAction[];
|
|
21
21
|
onAction?: (id: string) => void;
|
|
22
22
|
children?: ReactNode;
|
|
23
|
+
/** Without the bordered card and the title row, for a page that is already framed; the title still labels the section. */
|
|
24
|
+
frameless?: boolean;
|
|
23
25
|
};
|
|
24
26
|
/** A card with a toned title row, any content, a note and buttons: the building block of rich chat messages. */
|
|
25
|
-
export declare const ActionCard: ({ title, tone, badge, note, actions, onAction, children }: ActionCardProps) => import("react").JSX.Element;
|
|
27
|
+
export declare const ActionCard: ({ title, tone, badge, note, actions, onAction, children, frameless }: ActionCardProps) => import("react").JSX.Element;
|
|
26
28
|
//#endregion
|
|
@@ -27,15 +27,15 @@ const frames = {
|
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
/** A card with a toned title row, any content, a note and buttons: the building block of rich chat messages. */
|
|
30
|
-
const ActionCard = ({ title, tone = "neutral", badge, note, actions = [], onAction, children }) => {
|
|
30
|
+
const ActionCard = ({ title, tone = "neutral", badge, note, actions = [], onAction, children, frameless = false }) => {
|
|
31
31
|
const [confirming, setConfirming] = useState();
|
|
32
32
|
const hints = actions.flatMap((action) => action.hint ? [action.hint] : []);
|
|
33
33
|
const click = (action) => action.confirm ? setConfirming(action) : onAction?.(action.id);
|
|
34
34
|
return /* @__PURE__ */ jsxs("section", {
|
|
35
35
|
"aria-label": title,
|
|
36
|
-
className: cn("shrink-0 overflow-hidden rounded-lg border bg-background whitespace-normal text-foreground shadow-sm", frames[tone].border),
|
|
36
|
+
className: frameless ? "whitespace-normal text-foreground" : cn("shrink-0 overflow-hidden rounded-lg border bg-background whitespace-normal text-foreground shadow-sm", frames[tone].border),
|
|
37
37
|
children: [
|
|
38
|
-
/* @__PURE__ */ jsxs("header", {
|
|
38
|
+
!frameless && /* @__PURE__ */ jsxs("header", {
|
|
39
39
|
className: cn("flex items-center gap-2 px-3 py-2", frames[tone].header),
|
|
40
40
|
children: [/* @__PURE__ */ jsx("h3", {
|
|
41
41
|
className: "min-w-0 flex-1 text-[13px] font-semibold",
|
|
@@ -46,7 +46,7 @@ const ActionCard = ({ title, tone = "neutral", badge, note, actions = [], onActi
|
|
|
46
46
|
})]
|
|
47
47
|
}),
|
|
48
48
|
(children || note || actions.length > 0) && /* @__PURE__ */ jsxs("div", {
|
|
49
|
-
className: "space-y-2.5 px-3 py-2.5",
|
|
49
|
+
className: cn("space-y-2.5", !frameless && "px-3 py-2.5"),
|
|
50
50
|
children: [
|
|
51
51
|
children,
|
|
52
52
|
note && /* @__PURE__ */ jsx("p", {
|
package/dist/app/app.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export type AppProps = {
|
|
|
28
28
|
/** How the plugins `/meta` names are loaded, for stories and tests; the page's `fetch` and `import()` by default. */
|
|
29
29
|
pluginLoader?: PluginLoader;
|
|
30
30
|
};
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The admin: sign-in first, or browsing as a visitor when the project has an anonymous role, then a sidebar from
|
|
33
|
+
* `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent.
|
|
34
|
+
*/
|
|
32
35
|
export declare const App: ({ auth, workspace, ...rest }: AppProps) => import("react").JSX.Element;
|
|
33
36
|
//#endregion
|
package/dist/app/app.js
CHANGED
|
@@ -48,9 +48,10 @@ const placeOf = (address) => address?.kind === "page" ? address.page : address?.
|
|
|
48
48
|
const recordOf = (address) => address?.kind === "record" ? address : void 0;
|
|
49
49
|
const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
50
50
|
const meta = useAdminMeta();
|
|
51
|
-
const { state, session, signOut, stopStaffSession, organizations: organizationSettings } = useAuth();
|
|
52
|
-
if (state.kind !== "signed-in") throw new Error("The shell is only rendered for a signed-in user");
|
|
53
|
-
const
|
|
51
|
+
const { state, session, signOut, showSignIn, stopStaffSession, organizations: organizationSettings } = useAuth();
|
|
52
|
+
if (state.kind !== "signed-in" && state.kind !== "visitor") throw new Error("The shell is only rendered for a signed-in user or a visitor");
|
|
53
|
+
const user = state.kind === "signed-in" ? state.user : void 0;
|
|
54
|
+
const staff = state.kind === "signed-in" ? state.staff : void 0;
|
|
54
55
|
const { path, params, basePath } = useRouter();
|
|
55
56
|
const { address, go, open } = useAppAddress();
|
|
56
57
|
const place = placeOf(address);
|
|
@@ -112,15 +113,15 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
112
113
|
url: runtimeUrl,
|
|
113
114
|
client: runtime
|
|
114
115
|
});
|
|
115
|
-
const role = useActiveRole(useCallback(() => reopenIn(basePath), [basePath]));
|
|
116
|
-
const acting = actingUser(user, role.current);
|
|
117
|
-
const admin = isAdmin(acting);
|
|
116
|
+
const role = useActiveRole(useCallback(() => reopenIn(basePath), [basePath]), Boolean(user));
|
|
117
|
+
const acting = user && actingUser(user, role.current);
|
|
118
|
+
const admin = acting ? isAdmin(acting) : false;
|
|
118
119
|
const pluginsOff = usePluginsOff();
|
|
119
|
-
const organizations = useOrganizations(Boolean(organizationSettings));
|
|
120
|
+
const organizations = useOrganizations(Boolean(organizationSettings && user));
|
|
120
121
|
const current = organizations.current;
|
|
121
122
|
const outsideOrganizations = Boolean(organizationSettings) && current !== void 0 && !current.organization;
|
|
122
|
-
const organizationPage = organizationSettings ? organizationPageTitles[path] : void 0;
|
|
123
|
-
const accountPage = path === "/-/account" ? "Sign-in & security" : path === "/-/sign-in-policy" ? "Sign-in policy" : organizationPage;
|
|
123
|
+
const organizationPage = organizationSettings && user ? organizationPageTitles[path] : void 0;
|
|
124
|
+
const accountPage = !user ? void 0 : path === "/-/account" ? "Sign-in & security" : path === "/-/sign-in-policy" ? "Sign-in policy" : organizationPage;
|
|
124
125
|
const breadcrumb = accountPage ? [accountPage] : breadcrumbFor({
|
|
125
126
|
basePath,
|
|
126
127
|
address,
|
|
@@ -130,7 +131,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
130
131
|
recordTitle: title,
|
|
131
132
|
recordLoading: titleLoading
|
|
132
133
|
});
|
|
133
|
-
const content = path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
|
|
134
|
+
const content = user && path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : user && path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
|
|
134
135
|
title: "Admins only",
|
|
135
136
|
children: "Only an admin can change how people sign in."
|
|
136
137
|
}) : organizationPage && path === "/-/organization" ? /* @__PURE__ */ jsx(OrganizationPage, {
|
|
@@ -156,7 +157,10 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
156
157
|
address.page,
|
|
157
158
|
"."
|
|
158
159
|
]
|
|
159
|
-
}) : drawnFrom && drawnFrom.state !== "ready" ? /* @__PURE__ */ jsx(SourceNotice, { status: drawnFrom }) : address.kind === "create" ? /* @__PURE__ */ jsx(CreatePage, {
|
|
160
|
+
}) : drawnFrom && drawnFrom.state !== "ready" ? /* @__PURE__ */ jsx(SourceNotice, { status: drawnFrom }) : address.kind === "create" ? /* @__PURE__ */ jsx(CreatePage, {
|
|
161
|
+
resource: address.resource,
|
|
162
|
+
values: address.values
|
|
163
|
+
}, `${address.resource}/new?${new URLSearchParams(address.values).toString()}`) : address.kind === "record" ? /* @__PURE__ */ jsx(RecordPage, {
|
|
160
164
|
resource: address.resource,
|
|
161
165
|
recordKey: address.key
|
|
162
166
|
}, `${address.resource}/${address.key}`) : /* @__PURE__ */ jsx(ListPage, { address }, address.resource);
|
|
@@ -164,15 +168,16 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
164
168
|
sidebarMode: sidebarMode ?? "text-small",
|
|
165
169
|
activeItem: place ?? "",
|
|
166
170
|
breadcrumb,
|
|
167
|
-
user: userToShell(acting),
|
|
168
|
-
onSignOut: () => void signOut(),
|
|
171
|
+
user: acting && userToShell(acting),
|
|
172
|
+
onSignOut: user && (() => void signOut()),
|
|
173
|
+
onSignIn: user ? void 0 : showSignIn,
|
|
169
174
|
workspace: current?.organization?.name ?? workspace,
|
|
170
175
|
navGroups: groups,
|
|
171
176
|
bottomBar: bottomBarFromMeta(meta, basePath, place),
|
|
172
177
|
userMenu: [
|
|
173
178
|
...userMenuFromMeta(meta, basePath, place),
|
|
174
|
-
...organizationSettings ? organizationMenuItems(basePath, path, current) : [],
|
|
175
|
-
...accountMenuItems(basePath, path, admin)
|
|
179
|
+
...organizationSettings && user ? organizationMenuItems(basePath, path, current) : [],
|
|
180
|
+
...user ? accountMenuItems(basePath, path, admin) : []
|
|
176
181
|
],
|
|
177
182
|
organizations: organizationSettings && {
|
|
178
183
|
...current?.organization && { current: {
|
|
@@ -231,12 +236,12 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
231
236
|
});
|
|
232
237
|
};
|
|
233
238
|
const Authenticated = ({ baseUrl, client, sources, basePath, initialUrl, workspace, sidebarMode, ui, assistant, runtime, pluginLoader }) => {
|
|
234
|
-
const { session, markSignedOut } = useAuth();
|
|
239
|
+
const { state, session, markSignedOut } = useAuth();
|
|
235
240
|
return /* @__PURE__ */ jsx(ApiProvider, {
|
|
236
241
|
baseUrl,
|
|
237
242
|
client,
|
|
238
243
|
sources,
|
|
239
|
-
token: session.token,
|
|
244
|
+
token: state.kind === "visitor" ? void 0 : session.token,
|
|
240
245
|
onUnauthenticated: markSignedOut,
|
|
241
246
|
children: /* @__PURE__ */ jsx(Router, {
|
|
242
247
|
basePath,
|
|
@@ -254,7 +259,10 @@ const Authenticated = ({ baseUrl, client, sources, basePath, initialUrl, workspa
|
|
|
254
259
|
})
|
|
255
260
|
});
|
|
256
261
|
};
|
|
257
|
-
/**
|
|
262
|
+
/**
|
|
263
|
+
* The admin: sign-in first, or browsing as a visitor when the project has an anonymous role, then a sidebar from
|
|
264
|
+
* `/meta`, `/:page` composed pages, `/:resource` lists and `/:resource/:key` records. Fills its parent.
|
|
265
|
+
*/
|
|
258
266
|
const App = ({ auth, workspace, ...rest }) => /* @__PURE__ */ jsx(AuthProvider, {
|
|
259
267
|
session: auth,
|
|
260
268
|
children: /* @__PURE__ */ jsx(AuthGate, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useClient } from "../../data/api-provider.js";
|
|
2
2
|
import { recordKey } from "../../live/model-helpers.js";
|
|
3
3
|
import { humanize } from "../../live/naming.js";
|
|
4
|
+
import { fetchLabels } from "../../live/relation-labels.js";
|
|
4
5
|
import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
|
|
5
6
|
import { useAppAddress } from "../use-app-address.js";
|
|
6
7
|
import { FieldEditor } from "../field-editor.js";
|
|
@@ -8,14 +9,14 @@ import { can } from "../permissions.js";
|
|
|
8
9
|
import { useLiveSave } from "../use-live-save.js";
|
|
9
10
|
import { CreateRecordForm } from "./create-record-form.js";
|
|
10
11
|
import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
|
|
11
|
-
import { useState } from "react";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
12
13
|
import { isApiError } from "@protobase/client";
|
|
13
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
15
|
import { useQueryClient } from "@tanstack/react-query";
|
|
15
16
|
//#region src/app/create/create-page.tsx
|
|
16
17
|
const without = (source, name) => Object.fromEntries(Object.entries(source).filter(([key]) => key !== name));
|
|
17
|
-
/** `/:resource/new`: a form from the view's layout
|
|
18
|
-
const CreatePage = ({ resource }) => {
|
|
18
|
+
/** `/:resource/new`: a form from the view's layout, filled in with the address's `values`; on success it opens the new record. */
|
|
19
|
+
const CreatePage = ({ resource, values }) => {
|
|
19
20
|
const meta = useAdminMeta();
|
|
20
21
|
const model = meta.resources[resource];
|
|
21
22
|
const permissions = usePermissions(resource);
|
|
@@ -35,9 +36,12 @@ const CreatePage = ({ resource }) => {
|
|
|
35
36
|
"."
|
|
36
37
|
]
|
|
37
38
|
});
|
|
38
|
-
return /* @__PURE__ */ jsx(LoadedCreate, {
|
|
39
|
+
return /* @__PURE__ */ jsx(LoadedCreate, {
|
|
40
|
+
resource,
|
|
41
|
+
values
|
|
42
|
+
});
|
|
39
43
|
};
|
|
40
|
-
const LoadedCreate = ({ resource }) => {
|
|
44
|
+
const LoadedCreate = ({ resource, values }) => {
|
|
41
45
|
const meta = useAdminMeta();
|
|
42
46
|
const model = meta.resources[resource];
|
|
43
47
|
const view = meta.views[resource];
|
|
@@ -47,8 +51,30 @@ const LoadedCreate = ({ resource }) => {
|
|
|
47
51
|
const noun = view?.names?.singular ?? humanize(resource);
|
|
48
52
|
const feedback = useLiveSave(view?.saveFeedback ?? "toast", `New ${noun.toLowerCase()}`);
|
|
49
53
|
const sections = createSections(model, view);
|
|
50
|
-
const [draft, setDraft] = useState(() => initialDraft(model, sections));
|
|
54
|
+
const [draft, setDraft] = useState(() => initialDraft(model, sections, values));
|
|
51
55
|
const [errors, setErrors] = useState({});
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
for (const [name, key] of Object.entries(values ?? {})) {
|
|
58
|
+
const target = meta.resources[model.fields[name]?.relation?.resource ?? ""];
|
|
59
|
+
if (!target) continue;
|
|
60
|
+
fetchLabels(queryClient, client, target, [key], meta.views[target.name]).then((labels) => {
|
|
61
|
+
const label = labels.get(key);
|
|
62
|
+
if (label) setDraft((current) => isChosen(current[name]) && current[name].key === key ? {
|
|
63
|
+
...current,
|
|
64
|
+
[name]: {
|
|
65
|
+
key,
|
|
66
|
+
label
|
|
67
|
+
}
|
|
68
|
+
} : current);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}, [
|
|
72
|
+
values,
|
|
73
|
+
meta,
|
|
74
|
+
model,
|
|
75
|
+
client,
|
|
76
|
+
queryClient
|
|
77
|
+
]);
|
|
52
78
|
const change = (name, value) => {
|
|
53
79
|
setErrors((current) => without(current, name));
|
|
54
80
|
setDraft((current) => ({
|
|
@@ -42,9 +42,22 @@ const createBody = (model, resources, draft) => Object.fromEntries(Object.entrie
|
|
|
42
42
|
}));
|
|
43
43
|
/** A field is required when it can be neither empty nor filled in by a default. */
|
|
44
44
|
const isRequired = (field) => !field.nullable && !field.default && field.type !== "boolean";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const givenValue = (field, value) => {
|
|
46
|
+
if (field.type === "relation") return {
|
|
47
|
+
key: value,
|
|
48
|
+
label: value
|
|
49
|
+
};
|
|
50
|
+
if (field.type === "boolean") return value === "true";
|
|
51
|
+
return value;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The form's starting values: the values the address gives (`/notes/new?listing_id=42`), for the fields the form has,
|
|
55
|
+
* else literal defaults from `/meta`. Database defaults stay empty so the database fills them.
|
|
56
|
+
*/
|
|
57
|
+
const initialDraft = (model, sections, given = {}) => Object.fromEntries(sections.flatMap((section) => section.fields.flatMap((name) => {
|
|
58
|
+
const field = model.fields[name];
|
|
59
|
+
if (Object.hasOwn(given, name)) return [[name, givenValue(field, given[name])]];
|
|
60
|
+
const fallback = field.default;
|
|
48
61
|
if (!fallback || !("value" in fallback) || fallback.value === null || fallback.value === void 0) return [];
|
|
49
62
|
return [[name, typeof fallback.value === "boolean" ? fallback.value : String(fallback.value)]];
|
|
50
63
|
})));
|
|
@@ -4,6 +4,7 @@ import { humanize } from "../../live/naming.js";
|
|
|
4
4
|
import { useProjectUi } from "../pages/project-ui.js";
|
|
5
5
|
import { cardFields } from "./list-displays.js";
|
|
6
6
|
import { AppLink } from "../use-app-address.js";
|
|
7
|
+
import { fileShown, isPreviewable } from "../../live/file-values.js";
|
|
7
8
|
import { displayValue } from "../../live/display-value.js";
|
|
8
9
|
import { shownWidget } from "../field-types/shown-widget.js";
|
|
9
10
|
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
@@ -13,12 +14,25 @@ import { DisplayFooter, DisplayState } from "./display-frame.js";
|
|
|
13
14
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
15
|
//#region src/app/displays/cards-display.tsx
|
|
15
16
|
const grid = "grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-3";
|
|
17
|
+
const CardImage = ({ value }) => {
|
|
18
|
+
const file = fileShown(value);
|
|
19
|
+
const url = file && "url" in file && typeof file.url === "string" && isPreviewable(file.type) ? file.url : void 0;
|
|
20
|
+
return /* @__PURE__ */ jsx("span", {
|
|
21
|
+
className: "-mx-3 -mt-3 mb-1.5 block aspect-[4/3] overflow-hidden rounded-t-lg border-b border-border bg-muted",
|
|
22
|
+
children: url && /* @__PURE__ */ jsx("img", {
|
|
23
|
+
src: url,
|
|
24
|
+
alt: "",
|
|
25
|
+
loading: "lazy",
|
|
26
|
+
className: "size-full object-cover"
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
};
|
|
16
30
|
/**
|
|
17
|
-
* A card per record, opening its page: the title over the subtitle, the badge beside it and small meta fields below;
|
|
31
|
+
* A card per record, opening its page: its image across the top, the title over the subtitle, the badge beside it and small meta fields below;
|
|
18
32
|
* or, with a `card` component, what it draws of the row, the built-in content again when it is missing or throws.
|
|
19
33
|
*/
|
|
20
34
|
const CardsDisplay = ({ model, view, display, list }) => {
|
|
21
|
-
const { title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
35
|
+
const { image, title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
22
36
|
const label = (name) => view?.fields[name]?.label ?? humanize(name);
|
|
23
37
|
const text = (row, name) => {
|
|
24
38
|
const field = model.fields[name];
|
|
@@ -52,6 +66,7 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
52
66
|
children: list.rows.map((row) => {
|
|
53
67
|
const status = badge === void 0 ? void 0 : row.record[badge];
|
|
54
68
|
const builtIn = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
69
|
+
image !== void 0 && /* @__PURE__ */ jsx(CardImage, { value: row.record[image] }),
|
|
55
70
|
/* @__PURE__ */ jsxs("span", {
|
|
56
71
|
className: "flex items-start justify-between gap-2",
|
|
57
72
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -30,6 +30,7 @@ const displayChoices = (view) => [{
|
|
|
30
30
|
}))];
|
|
31
31
|
/** The fields a cards display shows, its title the view's title field when it names none. */
|
|
32
32
|
const cardFields = (model, view, display) => ({
|
|
33
|
+
image: display.image,
|
|
33
34
|
title: display.title ?? recordTitleField(model, view),
|
|
34
35
|
subtitle: display.subtitle,
|
|
35
36
|
badge: display.badge,
|
|
@@ -38,8 +39,9 @@ const cardFields = (model, view, display) => ({
|
|
|
38
39
|
/** Every field a display shows, the fields its rows are loaded and labelled for. */
|
|
39
40
|
const shownFields = (model, view, display) => {
|
|
40
41
|
if (display.kind === "custom") return display.fields;
|
|
41
|
-
const { title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
42
|
+
const { image, title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
42
43
|
return [
|
|
44
|
+
image,
|
|
43
45
|
title,
|
|
44
46
|
subtitle,
|
|
45
47
|
badge,
|
|
@@ -2,6 +2,7 @@ import { cn } from "../../lib/cn.js";
|
|
|
2
2
|
import { Badge } from "../../primitives/badge.js";
|
|
3
3
|
import { keys } from "../../data/query-keys.js";
|
|
4
4
|
import { useClient } from "../../data/api-provider.js";
|
|
5
|
+
import { fetchLabels } from "../../live/relation-labels.js";
|
|
5
6
|
import { useAdminMeta } from "../meta-gate.js";
|
|
6
7
|
import { AppLink } from "../use-app-address.js";
|
|
7
8
|
import { fileShown } from "../../live/file-values.js";
|
|
@@ -9,7 +10,6 @@ import { displayValue } from "../../live/display-value.js";
|
|
|
9
10
|
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
10
11
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
11
12
|
import { toneFor } from "../../live/columns-from-model.js";
|
|
12
|
-
import { fetchLabels } from "../../live/relation-labels.js";
|
|
13
13
|
import { jsx } from "react/jsx-runtime";
|
|
14
14
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
15
15
|
//#region src/app/pages/field-value.tsx
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "../../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../../data/api-provider.js";
|
|
3
|
-
import { useAdminMeta } from "../meta-gate.js";
|
|
4
3
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
4
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
5
5
|
import { useQueries, useQueryClient } from "@tanstack/react-query";
|
|
6
6
|
//#region src/app/pages/use-relation-labels.ts
|
|
7
7
|
/** Names for the relation columns of the rows on screen, one list call per column, by column then key. */
|
|
@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
|
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/app-shell/app-shell.tsx
|
|
10
10
|
const drawerId = "app-drawer";
|
|
11
|
-
const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, organizations, roles, search, className, children }) => {
|
|
11
|
+
const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, onSignIn, organizations, roles, search, className, children }) => {
|
|
12
12
|
const desktop = useIsDesktop();
|
|
13
13
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
14
14
|
useEffect(() => {
|
|
@@ -26,6 +26,7 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
26
26
|
userMenu,
|
|
27
27
|
onNavigate,
|
|
28
28
|
onSignOut,
|
|
29
|
+
onSignIn,
|
|
29
30
|
organizations,
|
|
30
31
|
roles
|
|
31
32
|
});
|
|
@@ -5,9 +5,10 @@ import { ProfileMenu } from "./profile-menu.js";
|
|
|
5
5
|
import { isTextMode, sidebarWidth } from "./sidebar-mode.js";
|
|
6
6
|
import { SidebarItem } from "./sidebar-item.js";
|
|
7
7
|
import { SidebarGroup } from "./sidebar-group.js";
|
|
8
|
+
import { LogIn } from "lucide-react";
|
|
8
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
10
|
//#region src/app-shell/sidebar.tsx
|
|
10
|
-
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, organizations, roles }) => {
|
|
11
|
+
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, onSignIn, organizations, roles }) => {
|
|
11
12
|
const expanding = mode === "icon-expand";
|
|
12
13
|
const expanded = expanding && forceExpanded;
|
|
13
14
|
const inline = isTextMode(mode) || expanding;
|
|
@@ -47,7 +48,7 @@ const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenu
|
|
|
47
48
|
}),
|
|
48
49
|
/* @__PURE__ */ jsx("div", {
|
|
49
50
|
className: cn("shrink-0 border-t p-2", mode !== "text-large" && mode !== "text-small" && !expanding && "flex justify-center"),
|
|
50
|
-
children: /* @__PURE__ */ jsx(ProfileMenu, {
|
|
51
|
+
children: user ? /* @__PURE__ */ jsx(ProfileMenu, {
|
|
51
52
|
user,
|
|
52
53
|
compact: !inline,
|
|
53
54
|
open: profileMenuOpen,
|
|
@@ -56,6 +57,15 @@ const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenu
|
|
|
56
57
|
onSignOut,
|
|
57
58
|
organizations,
|
|
58
59
|
roles
|
|
60
|
+
}) : onSignIn && /* @__PURE__ */ jsxs("button", {
|
|
61
|
+
type: "button",
|
|
62
|
+
onClick: onSignIn,
|
|
63
|
+
"aria-label": "Sign in",
|
|
64
|
+
className: "flex min-h-11 w-full items-center gap-2.5 rounded-md p-1.5 text-left text-[13px] font-medium text-muted-foreground hover:bg-muted hover:text-foreground md:min-h-0",
|
|
65
|
+
children: [/* @__PURE__ */ jsx(LogIn, { className: "size-4 shrink-0" }), inline && /* @__PURE__ */ jsx("span", {
|
|
66
|
+
className: "whitespace-nowrap",
|
|
67
|
+
children: "Sign in"
|
|
68
|
+
})]
|
|
59
69
|
})
|
|
60
70
|
})
|
|
61
71
|
]
|
package/dist/auth/auth-gate.js
CHANGED
|
@@ -53,12 +53,12 @@ const forgetStaffLink = () => window.history.replaceState(window.history.state,
|
|
|
53
53
|
const forgetSignInLink = () => window.history.replaceState(window.history.state, "", withoutSignInLink(window.location.href));
|
|
54
54
|
const forgetInvitationLink = () => window.history.replaceState(window.history.state, "", withoutInvitationLink(window.location.href));
|
|
55
55
|
/**
|
|
56
|
-
* Shows the app when the user is signed in (or the server has no login),
|
|
57
|
-
* steps, or the page an emailed reset link, an invitation link or a
|
|
58
|
-
* account signs in first and comes back to the invitation.
|
|
56
|
+
* Shows the app when the user is signed in (or the server has no login), or to a visitor when the project lets them
|
|
57
|
+
* browse, and the right page otherwise: the sign-in steps, or the page an emailed reset link, an invitation link or a
|
|
58
|
+
* staff link opened. An invited person with an account signs in first and comes back to the invitation.
|
|
59
59
|
*/
|
|
60
60
|
const AuthGate = ({ workspace, children }) => {
|
|
61
|
-
const { state, recheck, passwordReset, signOut, signInProviders } = useAuth();
|
|
61
|
+
const { state, recheck, passwordReset, signOut, signInProviders, showSignIn, browse } = useAuth();
|
|
62
62
|
const [page, setPage] = useState(firstPage);
|
|
63
63
|
useEffect(() => {
|
|
64
64
|
if (state.kind === "signed-in" || state.kind === "two-factor" || state.kind === "setup-required") setPage((current) => {
|
|
@@ -86,7 +86,7 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
86
86
|
};
|
|
87
87
|
const backToSignIn = () => {
|
|
88
88
|
setPage({ kind: "sign-in" });
|
|
89
|
-
signOut();
|
|
89
|
+
signOut().then(showSignIn);
|
|
90
90
|
};
|
|
91
91
|
switch (state.kind) {
|
|
92
92
|
case "loading": return /* @__PURE__ */ jsxs("div", {
|
|
@@ -118,18 +118,23 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
118
118
|
setPage({ kind: "sign-in" });
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
|
-
|
|
121
|
+
const signedOut = state.kind === "signed-out" || state.kind === "visitor";
|
|
122
|
+
if (page.kind === "invitation" && (state.kind === "signed-in" || signedOut && !page.signingIn)) return /* @__PURE__ */ jsx(InvitationPage, {
|
|
122
123
|
token: page.token,
|
|
123
124
|
onDone: () => {
|
|
124
125
|
forgetInvitationLink();
|
|
125
126
|
setPage({ kind: "sign-in" });
|
|
126
127
|
},
|
|
127
|
-
onSignIn: () =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
onSignIn: () => {
|
|
129
|
+
setPage({
|
|
130
|
+
...page,
|
|
131
|
+
signingIn: true
|
|
132
|
+
});
|
|
133
|
+
showSignIn();
|
|
134
|
+
}
|
|
131
135
|
});
|
|
132
136
|
if (state.kind === "signed-in") return children;
|
|
137
|
+
if (state.kind === "visitor" && page.kind === "sign-in" && !page.notice && !page.error && !page.providerError) return children;
|
|
133
138
|
if (state.kind === "two-factor") return /* @__PURE__ */ jsx(TwoFactorPage, {
|
|
134
139
|
methods: state.methods,
|
|
135
140
|
onBack: backToSignIn
|
|
@@ -156,7 +161,8 @@ const AuthGate = ({ workspace, children }) => {
|
|
|
156
161
|
onCodeSent: (email) => setPage({
|
|
157
162
|
kind: "email-code",
|
|
158
163
|
email
|
|
159
|
-
})
|
|
164
|
+
}),
|
|
165
|
+
onBrowse: browse
|
|
160
166
|
}, error);
|
|
161
167
|
};
|
|
162
168
|
//#endregion
|
|
@@ -19,6 +19,8 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
19
19
|
const [platformSignIn, setPlatformSignIn] = useState();
|
|
20
20
|
const [staffSignIn, setStaffSignIn] = useState();
|
|
21
21
|
const [organizations, setOrganizations] = useState();
|
|
22
|
+
const [anonymous, setAnonymous] = useState(false);
|
|
23
|
+
const signedOutState = useCallback(() => anonymous ? { kind: "visitor" } : { kind: "signed-out" }, [anonymous]);
|
|
22
24
|
const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
|
|
23
25
|
const signedOut = useRef(markSignedOut);
|
|
24
26
|
signedOut.current = markSignedOut;
|
|
@@ -49,9 +51,10 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
49
51
|
setPlatformSignIn(status.platformSignIn);
|
|
50
52
|
setStaffSignIn(status.staffSignIn);
|
|
51
53
|
setOrganizations(status.organizations);
|
|
54
|
+
setAnonymous(Boolean(status.anonymous));
|
|
52
55
|
if (status.needsAdmin) return setState({ kind: "needs-admin" });
|
|
53
56
|
const user = await session.session();
|
|
54
|
-
return user ? enter(user) : setState({ kind: "signed-out" });
|
|
57
|
+
return user ? enter(user) : setState({ kind: status.anonymous ? "visitor" : "signed-out" });
|
|
55
58
|
}).catch((error) => setState({
|
|
56
59
|
kind: "error",
|
|
57
60
|
message: error instanceof Error ? error.message : "The server did not answer"
|
|
@@ -73,6 +76,8 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
73
76
|
...organizations && { organizations },
|
|
74
77
|
markSignedOut,
|
|
75
78
|
recheck,
|
|
79
|
+
showSignIn: () => setState((current) => current.kind === "visitor" ? { kind: "signed-out" } : current),
|
|
80
|
+
...anonymous && { browse: () => setState((current) => current.kind === "signed-out" ? { kind: "visitor" } : current) },
|
|
76
81
|
signIn: async (email, password) => finish(await session.signIn(email, password)),
|
|
77
82
|
sendSignInCode: (email) => session.sendSignInCode(email),
|
|
78
83
|
signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
|
|
@@ -86,11 +91,11 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
86
91
|
},
|
|
87
92
|
stopStaffSession: async () => {
|
|
88
93
|
await session.staff.stop();
|
|
89
|
-
setState(
|
|
94
|
+
setState(signedOutState());
|
|
90
95
|
},
|
|
91
96
|
signOut: async () => {
|
|
92
97
|
await session.signOut();
|
|
93
|
-
setState(
|
|
98
|
+
setState(signedOutState());
|
|
94
99
|
}
|
|
95
100
|
}), [
|
|
96
101
|
state,
|
|
@@ -102,6 +107,8 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
102
107
|
platformSignIn,
|
|
103
108
|
staffSignIn,
|
|
104
109
|
organizations,
|
|
110
|
+
anonymous,
|
|
111
|
+
signedOutState,
|
|
105
112
|
markSignedOut,
|
|
106
113
|
recheck,
|
|
107
114
|
finish,
|
|
@@ -13,7 +13,7 @@ const Divider = () => /* @__PURE__ */ jsxs("div", {
|
|
|
13
13
|
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
|
|
14
14
|
]
|
|
15
15
|
});
|
|
16
|
-
const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith }) => {
|
|
16
|
+
const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith, onBrowse }) => {
|
|
17
17
|
const [email, setEmail] = useState("");
|
|
18
18
|
const [password, setPassword] = useState("");
|
|
19
19
|
const emailInput = useRef(null);
|
|
@@ -127,6 +127,12 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["pass
|
|
|
127
127
|
children: "Forgot password?"
|
|
128
128
|
})
|
|
129
129
|
]
|
|
130
|
+
}),
|
|
131
|
+
onBrowse && /* @__PURE__ */ jsx("button", {
|
|
132
|
+
type: "button",
|
|
133
|
+
onClick: onBrowse,
|
|
134
|
+
className: "mt-4 w-full text-center text-[13px] font-medium text-muted-foreground hover:text-foreground hover:underline",
|
|
135
|
+
children: "Continue without signing in"
|
|
130
136
|
})
|
|
131
137
|
]
|
|
132
138
|
});
|
|
@@ -5,7 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
import { AuthError } from "@protobase/client";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
//#region src/auth/sign-in-page.tsx
|
|
8
|
-
const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: initialError }) => {
|
|
8
|
+
const SignInPage = ({ workspace, notice, onForgotPassword, onBrowse, onCodeSent, error: initialError }) => {
|
|
9
9
|
const { signIn, signInSocial, signInProviders, signInMethods, passkeyDomains, sendSignInCode, signInWithPasskey } = useAuth();
|
|
10
10
|
const [busy, setBusy] = useState(false);
|
|
11
11
|
const [error, setError] = useState(initialError);
|
|
@@ -27,6 +27,7 @@ const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: in
|
|
|
27
27
|
notice,
|
|
28
28
|
methods: signInMethods,
|
|
29
29
|
onForgotPassword,
|
|
30
|
+
onBrowse,
|
|
30
31
|
providers: signInProviders,
|
|
31
32
|
onContinueWith: (provider) => void attempt(() => signInSocial(provider), signInMessage),
|
|
32
33
|
passkeyDomains,
|
|
@@ -2,9 +2,9 @@ import { formatMoney } from "../format/money.js";
|
|
|
2
2
|
import { keys } from "../data/query-keys.js";
|
|
3
3
|
import { useClient } from "../data/api-provider.js";
|
|
4
4
|
import { humanize } from "./naming.js";
|
|
5
|
+
import { fetchLabels } from "./relation-labels.js";
|
|
5
6
|
import { stateToText } from "./filter-string.js";
|
|
6
7
|
import { useHistogram } from "../data/use-aggregates.js";
|
|
7
|
-
import { fetchLabels } from "./relation-labels.js";
|
|
8
8
|
import { widgetFields } from "./view-fields.js";
|
|
9
9
|
import { useQueries, useQueryClient } from "@tanstack/react-query";
|
|
10
10
|
import { enumLabel } from "@protobase/schema";
|
package/dist/package.js
CHANGED
|
@@ -5,17 +5,22 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
5
5
|
import { AuthError } from "@protobase/client";
|
|
6
6
|
//#region src/roles/use-active-role.ts
|
|
7
7
|
const failureMessage = (failure, fallback) => failure instanceof AuthError ? authMessage(failure) : fallback;
|
|
8
|
-
/** The role the session acts with; a failed read or switch shows as a toast. */
|
|
9
|
-
const useActiveRole = (reopen) => {
|
|
8
|
+
/** The role the session acts with, read while `enabled` (someone is signed in); a failed read or switch shows as a toast. */
|
|
9
|
+
const useActiveRole = (reopen, enabled = true) => {
|
|
10
10
|
const { session } = useAuth();
|
|
11
11
|
const toast = useToast();
|
|
12
12
|
const [current, setCurrent] = useState();
|
|
13
13
|
useEffect(() => {
|
|
14
|
+
if (!enabled) return;
|
|
14
15
|
session.roles.current().then(setCurrent, (failure) => toast.show({
|
|
15
16
|
state: "error",
|
|
16
17
|
title: failureMessage(failure, "Could not read your roles")
|
|
17
18
|
}));
|
|
18
|
-
}, [
|
|
19
|
+
}, [
|
|
20
|
+
session,
|
|
21
|
+
toast,
|
|
22
|
+
enabled
|
|
23
|
+
]);
|
|
19
24
|
const switchTo = useCallback((name) => void session.roles.switchTo(name).then(reopen, (failure) => toast.show({
|
|
20
25
|
state: "error",
|
|
21
26
|
title: failureMessage(failure, "Could not switch the role")
|