@protobase/ui 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.js +67 -28
- package/dist/app/batch-failure.js +1 -1
- package/dist/app/bottom-bar-from-meta.js +37 -0
- package/dist/app/breadcrumb-for.js +18 -9
- package/dist/app/create/create-page.js +12 -6
- package/dist/app/current-address.d.ts +26 -0
- package/dist/app/current-address.js +41 -0
- package/dist/app/custom-section.js +37 -0
- package/dist/app/delete/record-diff.js +1 -1
- package/dist/app/displays/cards-display.js +76 -0
- package/dist/app/displays/custom-display.js +28 -0
- package/dist/app/displays/display-context.d.ts +21 -0
- package/dist/app/displays/display-context.js +21 -0
- package/dist/app/displays/display-controls.js +55 -0
- package/dist/app/displays/display-frame.js +30 -0
- package/dist/app/displays/list-displays.d.ts +11 -0
- package/dist/app/displays/list-displays.js +62 -0
- package/dist/app/displays/record-link.d.ts +19 -0
- package/dist/app/displays/record-link.js +25 -0
- package/dist/app/displays/use-display-rows.js +28 -0
- package/dist/app/field-editor.js +4 -4
- package/dist/app/index.html +3 -2
- package/dist/app/list-page.js +87 -37
- package/dist/app/main.js +3 -2
- package/dist/app/nav-from-meta.js +9 -2
- package/dist/app/nav-recent.js +6 -2
- package/dist/app/pages/action-href.js +1 -3
- package/dist/app/pages/blocks/card-row.js +1 -1
- package/dist/app/pages/blocks/link.js +20 -11
- package/dist/app/pages/blocks/modal-form.js +1 -1
- package/dist/app/pages/blocks/record-card.js +1 -1
- package/dist/app/pages/blocks/stat.js +7 -4
- package/dist/app/pages/blocks/table.js +1 -1
- package/dist/app/pages/field-value.js +8 -4
- package/dist/app/pages/merge-ui.js +3 -1
- package/dist/app/pages/problem.js +10 -0
- package/dist/app/pages/project-ui.d.ts +12 -4
- package/dist/app/pages/record-actions.js +6 -3
- package/dist/app/pages/record-table.js +7 -3
- package/dist/app/pages/render-node.js +3 -6
- package/dist/app/pages/ui-from-module.d.ts +9 -0
- package/dist/app/pages/ui-from-module.js +23 -0
- package/dist/app/pages/use-run-action.js +7 -7
- package/dist/app/record-page.js +51 -26
- package/dist/app/related-section.js +1 -1
- package/dist/app/router.js +4 -20
- package/dist/app/search/search-model.js +6 -2
- package/dist/app/search/use-global-search.js +1 -1
- package/dist/app/use-app-address.d.ts +24 -0
- package/dist/app/use-app-address.js +47 -0
- package/dist/app/use-related-records.js +8 -2
- package/dist/app/user-menu-from-meta.js +9 -2
- package/dist/app-shell/app-shell.d.ts +1 -0
- package/dist/app-shell/app-shell.js +27 -14
- package/dist/app-shell/bottom-bar.d.ts +1 -0
- package/dist/app-shell/bottom-bar.js +57 -0
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/profile-menu.js +35 -26
- package/dist/app-shell/sidebar.js +3 -2
- package/dist/app-shell/switch-group.js +29 -0
- package/dist/auth/auth-provider.js +5 -1
- package/dist/auth/sign-in-form.js +23 -12
- package/dist/auth/sign-in-page.js +3 -2
- package/dist/data/meta-data.d.ts +3 -1
- package/dist/data/use-meta.js +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/dist/live/audit-fields.js +5 -0
- package/dist/live/naming.js +2 -2
- package/dist/live/use-live-rows.js +7 -2
- package/dist/primitives/segmented.js +8 -5
- package/dist/roles/acting-user.js +11 -0
- package/dist/roles/use-active-role.js +33 -0
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@ import { humanize } from "../live/naming.js";
|
|
|
2
2
|
import { iconFor } from "./icons.js";
|
|
3
3
|
import { menuIcon } from "./menu-icons.js";
|
|
4
4
|
import { ExternalLink, LayoutDashboard } from "lucide-react";
|
|
5
|
+
import { appPath } from "@protobase/schema";
|
|
5
6
|
//#region src/app/user-menu-from-meta.ts
|
|
6
7
|
/** The resources and pages the user menu holds; the sidebar leaves them out. */
|
|
7
8
|
const userMenuResources = (meta) => new Set((meta.userMenu?.items ?? []).flatMap((item) => item.kind === "resource" ? [item.resource] : item.kind === "page" ? [item.page] : []));
|
|
@@ -20,7 +21,10 @@ const userMenuFromMeta = (meta, basePath, activeResource) => (meta.userMenu?.ite
|
|
|
20
21
|
id: item.page,
|
|
21
22
|
label: item.label ?? page?.title ?? humanize(item.page),
|
|
22
23
|
icon: menuIcon(item.icon ?? page?.icon, LayoutDashboard),
|
|
23
|
-
href:
|
|
24
|
+
href: appPath({
|
|
25
|
+
kind: "page",
|
|
26
|
+
page: item.page
|
|
27
|
+
}, basePath),
|
|
24
28
|
active: item.page === activeResource
|
|
25
29
|
};
|
|
26
30
|
}
|
|
@@ -28,7 +32,10 @@ const userMenuFromMeta = (meta, basePath, activeResource) => (meta.userMenu?.ite
|
|
|
28
32
|
id: item.resource,
|
|
29
33
|
label: item.label ?? meta.views[item.resource]?.names?.plural ?? humanize(item.resource),
|
|
30
34
|
icon: menuIcon(item.icon, iconFor(item.resource)),
|
|
31
|
-
href:
|
|
35
|
+
href: appPath({
|
|
36
|
+
kind: "list",
|
|
37
|
+
resource: item.resource
|
|
38
|
+
}, basePath),
|
|
32
39
|
active: item.resource === activeResource
|
|
33
40
|
};
|
|
34
41
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
3
|
+
import { BottomBar } from "./bottom-bar.js";
|
|
3
4
|
import { Drawer } from "./drawer.js";
|
|
4
5
|
import { Sidebar } from "./sidebar.js";
|
|
5
6
|
import { TopBar } from "./top-bar.js";
|
|
@@ -7,12 +8,13 @@ import { useEffect, useState } from "react";
|
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
//#region src/app-shell/app-shell.tsx
|
|
9
10
|
const drawerId = "app-drawer";
|
|
10
|
-
const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, onNavigate, onSignOut, organizations, search, className, children }) => {
|
|
11
|
+
const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, actions, rightPanel, forceExpanded, profileMenuOpen, navGroups, userMenu, bottomBar, onNavigate, onSignOut, organizations, roles, search, className, children }) => {
|
|
11
12
|
const desktop = useIsDesktop();
|
|
12
13
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
if (desktop) setDrawerOpen(false);
|
|
15
16
|
}, [desktop]);
|
|
17
|
+
const tabs = !desktop && bottomBar && bottomBar.length > 0 ? bottomBar : void 0;
|
|
16
18
|
const sidebar = (mode) => /* @__PURE__ */ jsx(Sidebar, {
|
|
17
19
|
mode,
|
|
18
20
|
activeItem,
|
|
@@ -24,7 +26,8 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
24
26
|
userMenu,
|
|
25
27
|
onNavigate,
|
|
26
28
|
onSignOut,
|
|
27
|
-
organizations
|
|
29
|
+
organizations,
|
|
30
|
+
roles
|
|
28
31
|
});
|
|
29
32
|
return /* @__PURE__ */ jsxs("div", {
|
|
30
33
|
className: cn("flex h-full min-h-0 w-full overflow-hidden bg-background", className),
|
|
@@ -37,18 +40,28 @@ const AppShell = ({ sidebarMode, activeItem, breadcrumb, user, workspace, action
|
|
|
37
40
|
}),
|
|
38
41
|
/* @__PURE__ */ jsxs("div", {
|
|
39
42
|
className: "flex min-w-0 flex-1 flex-col",
|
|
40
|
-
children: [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx(TopBar, {
|
|
45
|
+
breadcrumb,
|
|
46
|
+
onNavigate,
|
|
47
|
+
actions,
|
|
48
|
+
onMenu: desktop || tabs ? void 0 : () => setDrawerOpen(true),
|
|
49
|
+
menuOpen: drawerOpen,
|
|
50
|
+
menuControls: drawerId,
|
|
51
|
+
search
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsx("main", {
|
|
54
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
55
|
+
children
|
|
56
|
+
}),
|
|
57
|
+
tabs && /* @__PURE__ */ jsx(BottomBar, {
|
|
58
|
+
tabs,
|
|
59
|
+
onNavigate,
|
|
60
|
+
onMore: () => setDrawerOpen(true),
|
|
61
|
+
moreOpen: drawerOpen,
|
|
62
|
+
moreControls: drawerId
|
|
63
|
+
})
|
|
64
|
+
]
|
|
52
65
|
}),
|
|
53
66
|
rightPanel
|
|
54
67
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "lucide-react";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Ellipsis } from "lucide-react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app-shell/bottom-bar.tsx
|
|
5
|
+
const inApp = (event) => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
6
|
+
const tab = "flex min-h-[49px] min-w-0 flex-1 flex-col items-center justify-center gap-1 pb-1 pt-1.5 text-[10px] font-medium leading-none";
|
|
7
|
+
/**
|
|
8
|
+
* Tabs along the bottom of a phone, as on iOS: icon above label, the current screen in the accent colour, and "More"
|
|
9
|
+
* last, which opens the navigation drawer and is the current tab on screens the others do not hold. The padding below
|
|
10
|
+
* keeps the tabs clear of the iPhone home indicator.
|
|
11
|
+
*/
|
|
12
|
+
const BottomBar = ({ tabs, onNavigate, onMore, moreOpen, moreControls }) => {
|
|
13
|
+
const moreCurrent = !tabs.some((item) => item.active);
|
|
14
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
15
|
+
"aria-label": "Tabs",
|
|
16
|
+
className: "flex shrink-0 border-t bg-background pb-[env(safe-area-inset-bottom)] md:hidden",
|
|
17
|
+
children: [tabs.map((item) => {
|
|
18
|
+
const Icon = item.icon;
|
|
19
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
20
|
+
href: item.href,
|
|
21
|
+
"aria-current": item.active ? "page" : void 0,
|
|
22
|
+
...item.external ? {
|
|
23
|
+
target: "_blank",
|
|
24
|
+
rel: "noreferrer"
|
|
25
|
+
} : {},
|
|
26
|
+
onClick: (event) => {
|
|
27
|
+
if (item.external || !onNavigate || !inApp(event)) return;
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
onNavigate(item.href);
|
|
30
|
+
},
|
|
31
|
+
className: cn(tab, item.active ? "text-primary-text" : "text-muted-foreground"),
|
|
32
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
33
|
+
className: "size-6",
|
|
34
|
+
strokeWidth: item.active ? 2.2 : 1.9,
|
|
35
|
+
"aria-hidden": true
|
|
36
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "max-w-full truncate px-0.5",
|
|
38
|
+
children: item.label
|
|
39
|
+
})]
|
|
40
|
+
}, item.id);
|
|
41
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
42
|
+
type: "button",
|
|
43
|
+
"aria-expanded": moreOpen,
|
|
44
|
+
"aria-controls": moreControls,
|
|
45
|
+
"data-current": moreCurrent || void 0,
|
|
46
|
+
onClick: onMore,
|
|
47
|
+
className: cn(tab, moreCurrent ? "text-primary-text" : "text-muted-foreground"),
|
|
48
|
+
children: [/* @__PURE__ */ jsx(Ellipsis, {
|
|
49
|
+
className: "size-6",
|
|
50
|
+
strokeWidth: moreCurrent ? 2.2 : 1.9,
|
|
51
|
+
"aria-hidden": true
|
|
52
|
+
}), "More"]
|
|
53
|
+
})]
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { BottomBar };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { Avatar } from "../primitives/avatar.js";
|
|
3
3
|
import { Badge } from "../primitives/badge.js";
|
|
4
|
+
import { SwitchGroup } from "./switch-group.js";
|
|
4
5
|
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
import {
|
|
6
|
+
import { ChevronsUpDown, LogOut } from "lucide-react";
|
|
6
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/app-shell/profile-menu.tsx
|
|
8
9
|
const menuRow = "flex min-h-11 w-full items-center gap-2.5 px-3 py-1.5 text-left text-[13px] text-muted-foreground hover:bg-muted md:min-h-0";
|
|
9
10
|
/** The person at the bottom of the sidebar; the menu is fixed to the viewport so a narrow sidebar cannot clip it. */
|
|
10
|
-
const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate, onSignOut, organizations }) => {
|
|
11
|
+
const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate, onSignOut, organizations, roles }) => {
|
|
12
|
+
const activeRole = roles?.roles.find((role) => role.name === roles.active);
|
|
11
13
|
const [local, setLocal] = useState(false);
|
|
12
14
|
const open = controlled ?? local;
|
|
13
15
|
const root = useRef(null);
|
|
@@ -77,30 +79,37 @@ const ProfileMenu = ({ user, compact, open: controlled, items = [], onNavigate,
|
|
|
77
79
|
children: user.email
|
|
78
80
|
})]
|
|
79
81
|
}),
|
|
80
|
-
organizations && (organizations.current || organizations.others.length > 0) && /* @__PURE__ */
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
82
|
+
organizations && (organizations.current || organizations.others.length > 0) && /* @__PURE__ */ jsx(SwitchGroup, {
|
|
83
|
+
label: "Organizations",
|
|
84
|
+
...organizations.current && { current: {
|
|
85
|
+
id: organizations.current.id,
|
|
86
|
+
label: organizations.current.name
|
|
87
|
+
} },
|
|
88
|
+
others: organizations.others.map(({ id, name }) => ({
|
|
89
|
+
id,
|
|
90
|
+
label: name
|
|
91
|
+
})),
|
|
92
|
+
rowClassName: menuRow,
|
|
93
|
+
onSwitch: (id) => {
|
|
94
|
+
setLocal(false);
|
|
95
|
+
organizations.onSwitch(id);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
roles && roles.roles.length > 1 && /* @__PURE__ */ jsx(SwitchGroup, {
|
|
99
|
+
label: "Roles",
|
|
100
|
+
...activeRole && { current: {
|
|
101
|
+
id: activeRole.name,
|
|
102
|
+
label: activeRole.label
|
|
103
|
+
} },
|
|
104
|
+
others: roles.roles.filter((role) => role !== activeRole).map(({ name, label }) => ({
|
|
105
|
+
id: name,
|
|
106
|
+
label
|
|
107
|
+
})),
|
|
108
|
+
rowClassName: menuRow,
|
|
109
|
+
onSwitch: (name) => {
|
|
110
|
+
setLocal(false);
|
|
111
|
+
roles.onSwitch(name);
|
|
112
|
+
}
|
|
104
113
|
}),
|
|
105
114
|
items.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
106
115
|
className: cn(onSignOut && "border-b"),
|
|
@@ -7,7 +7,7 @@ import { SidebarItem } from "./sidebar-item.js";
|
|
|
7
7
|
import { SidebarGroup } from "./sidebar-group.js";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/app-shell/sidebar.tsx
|
|
10
|
-
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, organizations }) => {
|
|
10
|
+
const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenuOpen, groups = navGroups, userMenu, onNavigate, onSignOut, organizations, roles }) => {
|
|
11
11
|
const expanding = mode === "icon-expand";
|
|
12
12
|
const expanded = expanding && forceExpanded;
|
|
13
13
|
const inline = isTextMode(mode) || expanding;
|
|
@@ -54,7 +54,8 @@ const Sidebar = ({ mode, activeItem, user, workspace, forceExpanded, profileMenu
|
|
|
54
54
|
items: userMenu,
|
|
55
55
|
onNavigate,
|
|
56
56
|
onSignOut,
|
|
57
|
-
organizations
|
|
57
|
+
organizations,
|
|
58
|
+
roles
|
|
58
59
|
})
|
|
59
60
|
})
|
|
60
61
|
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/app-shell/switch-group.tsx
|
|
5
|
+
/** A group of the profile menu that switches between options: the current one checked, the others a click away. */
|
|
6
|
+
const SwitchGroup = ({ label, current, others, rowClassName, onSwitch }) => /* @__PURE__ */ jsxs("div", {
|
|
7
|
+
className: "border-b py-1",
|
|
8
|
+
role: "group",
|
|
9
|
+
"aria-label": label,
|
|
10
|
+
children: [current && /* @__PURE__ */ jsxs("div", {
|
|
11
|
+
className: cn(rowClassName, "text-foreground hover:bg-transparent"),
|
|
12
|
+
"aria-current": "true",
|
|
13
|
+
children: [/* @__PURE__ */ jsx(Check, { className: "size-4" }), /* @__PURE__ */ jsx("span", {
|
|
14
|
+
className: "truncate",
|
|
15
|
+
children: current.label
|
|
16
|
+
})]
|
|
17
|
+
}), others.map((option) => /* @__PURE__ */ jsxs("button", {
|
|
18
|
+
type: "button",
|
|
19
|
+
role: "menuitem",
|
|
20
|
+
onClick: () => onSwitch(option.id),
|
|
21
|
+
className: rowClassName,
|
|
22
|
+
children: [/* @__PURE__ */ jsx("span", { className: "size-4" }), /* @__PURE__ */ jsx("span", {
|
|
23
|
+
className: "truncate",
|
|
24
|
+
children: option.label
|
|
25
|
+
})]
|
|
26
|
+
}, option.id))]
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export { SwitchGroup };
|
|
@@ -14,6 +14,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
14
14
|
const [state, setState] = useState({ kind: "loading" });
|
|
15
15
|
const [signInMethods, setSignInMethods] = useState(["password"]);
|
|
16
16
|
const [passwordReset, setPasswordReset] = useState(false);
|
|
17
|
+
const [passkeyDomains, setPasskeyDomains] = useState([]);
|
|
17
18
|
const [socialProviders, setSocialProviders] = useState([]);
|
|
18
19
|
const [platformSignIn, setPlatformSignIn] = useState();
|
|
19
20
|
const [staffSignIn, setStaffSignIn] = useState();
|
|
@@ -43,6 +44,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
43
44
|
session.status().then(async (status) => {
|
|
44
45
|
setSignInMethods(status.signInMethods);
|
|
45
46
|
setPasswordReset(status.passwordReset);
|
|
47
|
+
setPasskeyDomains(status.passkeyDomains ?? []);
|
|
46
48
|
setSocialProviders(status.socialProviders);
|
|
47
49
|
setPlatformSignIn(status.platformSignIn);
|
|
48
50
|
setStaffSignIn(status.staffSignIn);
|
|
@@ -61,6 +63,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
61
63
|
session,
|
|
62
64
|
signInMethods,
|
|
63
65
|
passwordReset,
|
|
66
|
+
passkeyDomains,
|
|
64
67
|
socialProviders,
|
|
65
68
|
signInProviders: socialProviders.map((id) => ({
|
|
66
69
|
id,
|
|
@@ -73,7 +76,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
73
76
|
signIn: async (email, password) => finish(await session.signIn(email, password)),
|
|
74
77
|
sendSignInCode: (email) => session.sendSignInCode(email),
|
|
75
78
|
signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
|
|
76
|
-
signInWithPasskey: async () => enter(await session.signInWithPasskey()),
|
|
79
|
+
signInWithPasskey: async (domain) => enter(await session.signInWithPasskey(domain)),
|
|
77
80
|
verifyTwoFactor: async (input) => enter(await session.verifyTwoFactor(input)),
|
|
78
81
|
signInSocial: async (provider) => {
|
|
79
82
|
window.location.assign(await session.signInSocial(provider, new URL(withoutSignInLink(window.location.href), window.location.href).href));
|
|
@@ -94,6 +97,7 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
94
97
|
session,
|
|
95
98
|
signInMethods,
|
|
96
99
|
passwordReset,
|
|
100
|
+
passkeyDomains,
|
|
97
101
|
socialProviders,
|
|
98
102
|
platformSignIn,
|
|
99
103
|
staffSignIn,
|
|
@@ -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, onForgotPassword, providers = [], onContinueWith }) => {
|
|
16
|
+
const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, passkeyDomains = [], onForgotPassword, providers = [], onContinueWith }) => {
|
|
17
17
|
const [email, setEmail] = useState("");
|
|
18
18
|
const [password, setPassword] = useState("");
|
|
19
19
|
const emailInput = useRef(null);
|
|
@@ -36,17 +36,28 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["pass
|
|
|
36
36
|
}),
|
|
37
37
|
(offered.length > 0 || withPasskey) && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
38
38
|
className: "flex flex-col gap-2",
|
|
39
|
-
children: [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
children: [
|
|
40
|
+
offered.map((provider) => /* @__PURE__ */ jsxs(Button, {
|
|
41
|
+
onClick: () => onContinueWith?.(provider.id),
|
|
42
|
+
disabled: busy,
|
|
43
|
+
className: "min-h-10 w-full",
|
|
44
|
+
children: ["Continue with ", provider.name]
|
|
45
|
+
}, provider.id)),
|
|
46
|
+
withPasskey && /* @__PURE__ */ jsxs(Button, {
|
|
47
|
+
onClick: () => onPasskey?.(),
|
|
48
|
+
disabled: busy,
|
|
49
|
+
className: "min-h-10 w-full",
|
|
50
|
+
children: [/* @__PURE__ */ jsx(KeyRound, { className: "size-4" }), "Sign in with a passkey"]
|
|
51
|
+
}),
|
|
52
|
+
withPasskey && passkeyDomains.slice(1).map((domain) => /* @__PURE__ */ jsxs(Button, {
|
|
53
|
+
variant: "ghost",
|
|
54
|
+
size: "sm",
|
|
55
|
+
onClick: () => onPasskey?.(domain),
|
|
56
|
+
disabled: busy,
|
|
57
|
+
className: "w-full",
|
|
58
|
+
children: ["Use a passkey made for ", domain]
|
|
59
|
+
}, domain))
|
|
60
|
+
]
|
|
50
61
|
}), withPassword || withCode ? /* @__PURE__ */ jsx(Divider, {}) : error && /* @__PURE__ */ jsx("p", {
|
|
51
62
|
role: "alert",
|
|
52
63
|
className: "mt-4 text-xs font-medium text-danger-text",
|
|
@@ -6,7 +6,7 @@ import { AuthError } from "@protobase/client";
|
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
//#region src/auth/sign-in-page.tsx
|
|
8
8
|
const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: initialError }) => {
|
|
9
|
-
const { signIn, signInSocial, signInProviders, signInMethods, sendSignInCode, signInWithPasskey } = useAuth();
|
|
9
|
+
const { signIn, signInSocial, signInProviders, signInMethods, passkeyDomains, sendSignInCode, signInWithPasskey } = useAuth();
|
|
10
10
|
const [busy, setBusy] = useState(false);
|
|
11
11
|
const [error, setError] = useState(initialError);
|
|
12
12
|
const attempt = async (start, message) => {
|
|
@@ -29,7 +29,8 @@ const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent, error: in
|
|
|
29
29
|
onForgotPassword,
|
|
30
30
|
providers: signInProviders,
|
|
31
31
|
onContinueWith: (provider) => void attempt(() => signInSocial(provider), signInMessage),
|
|
32
|
-
|
|
32
|
+
passkeyDomains,
|
|
33
|
+
onPasskey: (domain) => void attempt(() => signInWithPasskey(domain), authMessage),
|
|
33
34
|
onSendCode: (email) => void attempt(async () => {
|
|
34
35
|
await sendSignInCode(email);
|
|
35
36
|
onCodeSent(email);
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ResourcePermissions } from "@protobase/client";
|
|
2
|
-
import { ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
2
|
+
import { BottomBarModel, ResourceModel, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
3
3
|
import { PageModel } from "@protobase/layout";
|
|
4
4
|
//#region src/data/meta-data.d.ts
|
|
5
5
|
export type MetaData = {
|
|
@@ -10,6 +10,8 @@ export type MetaData = {
|
|
|
10
10
|
pages: Record<string, PageModel>;
|
|
11
11
|
permissions: Record<string, ResourcePermissions>;
|
|
12
12
|
userMenu?: UserMenuModel;
|
|
13
|
+
/** The tabs of the user's bottom bar on phones, when one names their active role. */
|
|
14
|
+
bottomBar?: Omit<BottomBarModel, 'roles'>;
|
|
13
15
|
/** The assistant backend, when the app has one and the user has the admin or ai role. */
|
|
14
16
|
assistant?: {
|
|
15
17
|
url: string;
|
package/dist/data/use-meta.js
CHANGED
|
@@ -27,6 +27,7 @@ const useMeta = () => {
|
|
|
27
27
|
pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
|
|
28
28
|
permissions: result.meta.permissions ?? {},
|
|
29
29
|
...result.meta.userMenu && { userMenu: result.meta.userMenu },
|
|
30
|
+
...result.meta.bottomBar && { bottomBar: result.meta.bottomBar },
|
|
30
31
|
...result.meta.assistant && { assistant: result.meta.assistant },
|
|
31
32
|
...result.meta.runtime && { runtime: result.meta.runtime }
|
|
32
33
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,13 @@ import { useRecord } from "./data/use-record.js";
|
|
|
9
9
|
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
10
10
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
11
11
|
import { mergeUi } from "./app/pages/merge-ui.js";
|
|
12
|
+
import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
12
13
|
import { LayoutRecord, useLayoutRecord } from "./app/pages/record-scope.js";
|
|
14
|
+
import { CurrentAddress, ListAddress, ListChange } from "./app/current-address.js";
|
|
15
|
+
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
16
|
+
import { ListDisplayRow } from "./app/displays/list-displays.js";
|
|
17
|
+
import { ListDisplay, useListDisplay } from "./app/displays/display-context.js";
|
|
18
|
+
import { RecordLink } from "./app/displays/record-link.js";
|
|
13
19
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
14
20
|
import { AssistantDock, AssistantDockProps } from "./assistant-dock/assistant-dock.js";
|
|
15
21
|
import { PartView } from "./assistant-dock/part-view.js";
|
|
@@ -30,4 +36,4 @@ import { Step, StepList, StepState } from "./step-list/step-list.js";
|
|
|
30
36
|
import { DiffView, DiffViewProps } from "./diff-view/diff-view.js";
|
|
31
37
|
import { DiffLine, diffStats, parseDiff } from "./diff-view/diff.js";
|
|
32
38
|
import { CompactTable, CompactTableProps } from "./compact-table/compact-table.js";
|
|
33
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type DiffLine, DiffView, type DiffViewProps, FieldList, type LayoutRecord, type MetaData, PartView, type ProjectUi, SidePanel, type SidePanelProps, type Step, StepList, type StepState, defineUi, diffStats, isConflict, mergeUi, parseDiff, useApiToken, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useMeta, useRecord, useSeries, useUpdateRecord };
|
|
39
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, type BackendData, type BackendRefresh, type CardAction, type CardTone, ChatMessage, ChatThread, type ChatThreadProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, type DiffLine, DiffView, type DiffViewProps, FieldList, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, type MetaData, PartView, type ProjectUi, RecordLink, SidePanel, type SidePanelProps, type Step, StepList, type StepState, defineUi, diffStats, isConflict, mergeUi, parseDiff, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, useRecord, useSeries, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -18,10 +18,14 @@ import { AssistantDock } from "./assistant-dock/assistant-dock.js";
|
|
|
18
18
|
import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
19
19
|
import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
|
|
20
20
|
import { useMeta } from "./data/use-meta.js";
|
|
21
|
+
import { AppLink, useAppAddress } from "./app/use-app-address.js";
|
|
21
22
|
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
23
|
+
import { useListDisplay } from "./app/displays/display-context.js";
|
|
22
24
|
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
25
|
+
import { uiFromModule } from "./app/pages/ui-from-module.js";
|
|
23
26
|
import { mergeUi } from "./app/pages/merge-ui.js";
|
|
24
27
|
import { useList } from "./data/use-list.js";
|
|
25
28
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
26
29
|
import { App } from "./app/app.js";
|
|
27
|
-
|
|
30
|
+
import { RecordLink } from "./app/displays/record-link.js";
|
|
31
|
+
export { ActionCard, ApiProvider, App, AppLink, AssistantDock, ChatMessage, ChatThread, CompactTable, Composer, DiffView, FieldList, PartView, RecordLink, SidePanel, StepList, defineUi, diffStats, isConflict, mergeUi, parseDiff, uiFromModule, useApiToken, useAppAddress, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useHistogram, useLayoutRecord, useList, useListDisplay, useMeta, useRecord, useSeries, useUpdateRecord };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
//#region src/live/audit-fields.ts
|
|
2
|
+
/** The field that holds when a record was created or last updated: `created_at` or `createdAt`, as the model names it. */
|
|
3
|
+
const auditField = (model, kind) => [`${kind}_at`, `${kind}At`].find((name) => model.fields[name]);
|
|
4
|
+
//#endregion
|
|
5
|
+
export { auditField };
|
package/dist/live/naming.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//#region src/live/naming.ts
|
|
2
2
|
const capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
3
|
-
/** `companyId`
|
|
3
|
+
/** `companyId` and `company_id` become "Company", `discountPercent` and `discount_percent` become "Discount percent". */
|
|
4
4
|
const humanize = (name) => {
|
|
5
|
-
const words = name.replace(/(Id|Code)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
5
|
+
const words = name.replace(/(Id|Code|_id|_code)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/_+/g, " ").trim().toLowerCase();
|
|
6
6
|
return capitalise(words || name);
|
|
7
7
|
};
|
|
8
8
|
//#endregion
|
|
@@ -4,7 +4,10 @@ import { createGridLoader } from "./grid-loader.js";
|
|
|
4
4
|
import { useMemo } from "react";
|
|
5
5
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
6
|
//#region src/live/use-live-rows.ts
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* A grid row source backed by the API; `total` is the server's estimate from the first page. `page` loads one page of
|
|
9
|
+
* the same rows by number, and `key` names them in the query cache, for list displays that page through them.
|
|
10
|
+
*/
|
|
8
11
|
const useLiveRows = ({ resources, views, model, columns, filter, orderBy }) => {
|
|
9
12
|
const client = useClient();
|
|
10
13
|
const queryClient = useQueryClient();
|
|
@@ -38,7 +41,9 @@ const useLiveRows = ({ resources, views, model, columns, filter, orderBy }) => {
|
|
|
38
41
|
loadRows: loader.loadRows
|
|
39
42
|
} : void 0, [first.data, loader]),
|
|
40
43
|
error: first.error,
|
|
41
|
-
isLoading: first.isLoading
|
|
44
|
+
isLoading: first.isLoading,
|
|
45
|
+
page: loader.page,
|
|
46
|
+
key: [...keys.first(model.name, filter, orderBy), columnKey]
|
|
42
47
|
};
|
|
43
48
|
};
|
|
44
49
|
//#endregion
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
//#region src/primitives/segmented.tsx
|
|
4
|
-
const Segmented = ({ value, options, onChange }) => /* @__PURE__ */ jsx("span", {
|
|
4
|
+
const Segmented = ({ value, options, onChange, label }) => /* @__PURE__ */ jsx("span", {
|
|
5
|
+
role: "group",
|
|
6
|
+
"aria-label": label,
|
|
5
7
|
className: "inline-flex h-7 rounded-md bg-muted p-0.5",
|
|
6
|
-
children: options.map((option) => /* @__PURE__ */
|
|
8
|
+
children: options.map((option) => /* @__PURE__ */ jsxs("button", {
|
|
7
9
|
type: "button",
|
|
10
|
+
"aria-pressed": option.value === value,
|
|
8
11
|
onClick: () => onChange(option.value),
|
|
9
|
-
className: cn("rounded px-2.5 text-xs font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
|
|
10
|
-
children: option.label
|
|
12
|
+
className: cn("inline-flex items-center gap-1.5 rounded px-2.5 text-xs font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
|
|
13
|
+
children: [option.icon && /* @__PURE__ */ jsx(option.icon, { className: "size-3.5" }), option.label]
|
|
11
14
|
}, option.value))
|
|
12
15
|
});
|
|
13
16
|
//#endregion
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/roles/acting-user.ts
|
|
2
|
+
/**
|
|
3
|
+
* The signed-in user as the session acts: with the active role alone, as its tokens carry it, so what the shell shows
|
|
4
|
+
* by role follows a switch. As signed in until the active role is read, or for someone without a role.
|
|
5
|
+
*/
|
|
6
|
+
const actingUser = (user, role) => role?.active ? {
|
|
7
|
+
...user,
|
|
8
|
+
role: role.active
|
|
9
|
+
} : user;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { actingUser };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { authMessage } from "../auth/auth-messages.js";
|
|
2
|
+
import { useAuth } from "../auth/auth-provider.js";
|
|
3
|
+
import { useToast } from "../toasts/toast-provider.js";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { AuthError } from "@protobase/client";
|
|
6
|
+
//#region src/roles/use-active-role.ts
|
|
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) => {
|
|
10
|
+
const { session } = useAuth();
|
|
11
|
+
const toast = useToast();
|
|
12
|
+
const [current, setCurrent] = useState();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
session.roles.current().then(setCurrent, (failure) => toast.show({
|
|
15
|
+
state: "error",
|
|
16
|
+
title: failureMessage(failure, "Could not read your roles")
|
|
17
|
+
}));
|
|
18
|
+
}, [session, toast]);
|
|
19
|
+
const switchTo = useCallback((name) => void session.roles.switchTo(name).then(reopen, (failure) => toast.show({
|
|
20
|
+
state: "error",
|
|
21
|
+
title: failureMessage(failure, "Could not switch the role")
|
|
22
|
+
})), [
|
|
23
|
+
session,
|
|
24
|
+
toast,
|
|
25
|
+
reopen
|
|
26
|
+
]);
|
|
27
|
+
return {
|
|
28
|
+
...current && { current },
|
|
29
|
+
switchTo
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { useActiveRole };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The Protobase admin app and the React hooks and components for custom UI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@dnd-kit/core": "^6.3.1",
|
|
36
36
|
"@dnd-kit/sortable": "^10.0.0",
|
|
37
|
-
"@protobase/client": "0.
|
|
38
|
-
"@protobase/layout": "0.
|
|
39
|
-
"@protobase/schema": "0.
|
|
37
|
+
"@protobase/client": "0.11.0",
|
|
38
|
+
"@protobase/layout": "0.11.0",
|
|
39
|
+
"@protobase/schema": "0.11.0",
|
|
40
40
|
"@tanstack/react-query": "^5.104.1",
|
|
41
41
|
"@tanstack/react-table": "^9.2.6",
|
|
42
42
|
"@tanstack/react-virtual": "^3.14.13",
|