@protobase/ui 0.12.9 → 0.12.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/app/action-icon.js +3 -7
- 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 +58 -10
- package/dist/app/displays/list-displays.js +7 -3
- package/dist/app/field-editor.js +4 -0
- package/dist/app/named-icon.js +13 -0
- package/dist/app/pages/field-value.js +5 -5
- package/dist/app/pages/use-relation-labels.js +1 -1
- package/dist/app/record-page.js +4 -3
- package/dist/app/value-badge.js +14 -0
- 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/data-grid/cells.js +9 -6
- package/dist/lib/readable-text.js +23 -0
- package/dist/live/columns-from-model.js +3 -0
- package/dist/live/display-value.js +6 -3
- package/dist/live/use-filter-config.js +5 -3
- package/dist/live/value-color.js +8 -0
- package/dist/package.js +1 -1
- package/dist/primitives/badge.js +12 -3
- package/dist/roles/use-active-role.js +8 -3
- package/dist/shell/assets/app-B2rZN97s.js +65 -0
- package/dist/shell/assets/{index-CGPHR8-j.js → index-B7XSEKuk.js} +1 -1
- package/dist/shell/assets/index-otHceRXj.css +2 -0
- package/dist/shell/assets/{shared-protobase-ui-oolw3GIu.js → shared-protobase-ui-DhQ8wFhe.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-Ds2CYFLS.js +0 -65
- package/dist/shell/assets/index-BMIEmNDF.css +0 -2
package/dist/app/action-icon.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { namedIcon } from "./named-icon.js";
|
|
2
|
+
import { Zap } from "lucide-react";
|
|
2
3
|
//#region src/app/action-icon.ts
|
|
3
|
-
const
|
|
4
|
-
wallet: Wallet,
|
|
5
|
-
"file-text": FileText,
|
|
6
|
-
send: Send
|
|
7
|
-
};
|
|
8
|
-
const actionIcon = (name) => name ? byName[name] ?? Zap : Zap;
|
|
4
|
+
const actionIcon = (name) => namedIcon(name) ?? Zap;
|
|
9
5
|
//#endregion
|
|
10
6
|
export { actionIcon };
|
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,21 +4,47 @@ 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";
|
|
10
11
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
11
|
-
import {
|
|
12
|
+
import { namedIcon } from "../named-icon.js";
|
|
13
|
+
import { ValueBadge } from "../value-badge.js";
|
|
12
14
|
import { DisplayFooter, DisplayState } from "./display-frame.js";
|
|
13
15
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
16
|
//#region src/app/displays/cards-display.tsx
|
|
15
17
|
const grid = "grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-3";
|
|
18
|
+
const CardImage = ({ value }) => {
|
|
19
|
+
const file = fileShown(value);
|
|
20
|
+
const url = file && "url" in file && typeof file.url === "string" && isPreviewable(file.type) ? file.url : void 0;
|
|
21
|
+
return /* @__PURE__ */ jsx("span", {
|
|
22
|
+
className: "-mx-3 -mt-3 mb-1.5 block aspect-[4/3] overflow-hidden rounded-t-lg border-b border-border bg-muted",
|
|
23
|
+
children: url && /* @__PURE__ */ jsx("img", {
|
|
24
|
+
src: url,
|
|
25
|
+
alt: "",
|
|
26
|
+
loading: "lazy",
|
|
27
|
+
className: "size-full object-cover"
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
const MetaValue = ({ icon, children }) => {
|
|
32
|
+
const Icon = namedIcon(icon);
|
|
33
|
+
return /* @__PURE__ */ jsxs("dd", {
|
|
34
|
+
className: "flex items-center gap-1 tabular-nums",
|
|
35
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, {
|
|
36
|
+
className: "size-3.5 shrink-0",
|
|
37
|
+
"aria-hidden": true
|
|
38
|
+
}), children]
|
|
39
|
+
});
|
|
40
|
+
};
|
|
16
41
|
/**
|
|
17
|
-
* A card per record, opening its page: the title over the subtitle, the badge beside it
|
|
18
|
-
* or, with a `card` component, what it draws of the
|
|
42
|
+
* A card per record, opening its page: its image across the top, the title over the subtitle, the badge beside it,
|
|
43
|
+
* small meta fields below and the labels of its flags that are true; or, with a `card` component, what it draws of the
|
|
44
|
+
* row, the built-in content again when it is missing or throws.
|
|
19
45
|
*/
|
|
20
46
|
const CardsDisplay = ({ model, view, display, list }) => {
|
|
21
|
-
const { title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
47
|
+
const { image, title, subtitle, badge, meta, flags } = cardFields(model, view, display);
|
|
22
48
|
const label = (name) => view?.fields[name]?.label ?? humanize(name);
|
|
23
49
|
const text = (row, name) => {
|
|
24
50
|
const field = model.fields[name];
|
|
@@ -38,6 +64,17 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
38
64
|
fallback: text(row, name)
|
|
39
65
|
}) : text(row, name);
|
|
40
66
|
};
|
|
67
|
+
const metaValue = (row, name) => {
|
|
68
|
+
const field = model.fields[name];
|
|
69
|
+
const hints = view?.fields[name];
|
|
70
|
+
const value = row.record[name];
|
|
71
|
+
if (!field || hints?.format !== "badge" || shownWidget(fieldTypes, field, hints) || value === null || value === void 0 || value === "") return shownValue(row, name);
|
|
72
|
+
return /* @__PURE__ */ jsx(ValueBadge, {
|
|
73
|
+
value,
|
|
74
|
+
hints,
|
|
75
|
+
children: text(row, name)
|
|
76
|
+
});
|
|
77
|
+
};
|
|
41
78
|
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
42
79
|
return /* @__PURE__ */ jsxs(DisplayState, {
|
|
43
80
|
list,
|
|
@@ -51,14 +88,17 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
51
88
|
"aria-label": view?.names?.plural ?? humanize(model.name),
|
|
52
89
|
children: list.rows.map((row) => {
|
|
53
90
|
const status = badge === void 0 ? void 0 : row.record[badge];
|
|
91
|
+
const raised = flags.filter((name) => row.record[name] === true);
|
|
54
92
|
const builtIn = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
93
|
+
image !== void 0 && /* @__PURE__ */ jsx(CardImage, { value: row.record[image] }),
|
|
55
94
|
/* @__PURE__ */ jsxs("span", {
|
|
56
95
|
className: "flex items-start justify-between gap-2",
|
|
57
96
|
children: [/* @__PURE__ */ jsx("span", {
|
|
58
97
|
className: "min-w-0 truncate font-medium",
|
|
59
98
|
children: shownValue(row, title)
|
|
60
|
-
}), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(
|
|
61
|
-
|
|
99
|
+
}), badge !== void 0 && status !== null && status !== void 0 && /* @__PURE__ */ jsx(ValueBadge, {
|
|
100
|
+
value: status,
|
|
101
|
+
hints: view?.fields[badge],
|
|
62
102
|
children: text(row, badge)
|
|
63
103
|
})]
|
|
64
104
|
}),
|
|
@@ -67,17 +107,25 @@ const CardsDisplay = ({ model, view, display, list }) => {
|
|
|
67
107
|
children: shownValue(row, subtitle)
|
|
68
108
|
}),
|
|
69
109
|
meta.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
70
|
-
className: "mt-auto flex flex-wrap gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
110
|
+
className: "mt-auto flex flex-wrap items-center gap-x-3 gap-y-0.5 pt-1 text-xs text-muted-foreground",
|
|
71
111
|
children: meta.map((name) => /* @__PURE__ */ jsxs("div", {
|
|
72
112
|
className: "flex gap-1",
|
|
73
113
|
children: [/* @__PURE__ */ jsx("dt", {
|
|
74
114
|
className: "sr-only",
|
|
75
115
|
children: label(name)
|
|
76
|
-
}), /* @__PURE__ */ jsx(
|
|
77
|
-
|
|
78
|
-
children:
|
|
116
|
+
}), /* @__PURE__ */ jsx(MetaValue, {
|
|
117
|
+
icon: view?.fields[name]?.icon,
|
|
118
|
+
children: metaValue(row, name)
|
|
79
119
|
})]
|
|
80
120
|
}, name))
|
|
121
|
+
}),
|
|
122
|
+
raised.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
123
|
+
className: "flex flex-wrap gap-1",
|
|
124
|
+
"aria-label": "Features",
|
|
125
|
+
children: raised.map((name) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Badge, {
|
|
126
|
+
dot: false,
|
|
127
|
+
children: label(name)
|
|
128
|
+
}) }, name))
|
|
81
129
|
})
|
|
82
130
|
] });
|
|
83
131
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AppLink, {
|
|
@@ -30,20 +30,24 @@ 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,
|
|
36
|
-
meta: display.meta ?? []
|
|
37
|
+
meta: display.meta ?? [],
|
|
38
|
+
flags: display.flags ?? []
|
|
37
39
|
});
|
|
38
40
|
/** Every field a display shows, the fields its rows are loaded and labelled for. */
|
|
39
41
|
const shownFields = (model, view, display) => {
|
|
40
42
|
if (display.kind === "custom") return display.fields;
|
|
41
|
-
const { title, subtitle, badge, meta } = cardFields(model, view, display);
|
|
43
|
+
const { image, title, subtitle, badge, meta, flags } = cardFields(model, view, display);
|
|
42
44
|
return [
|
|
45
|
+
image,
|
|
43
46
|
title,
|
|
44
47
|
subtitle,
|
|
45
48
|
badge,
|
|
46
|
-
...meta
|
|
49
|
+
...meta,
|
|
50
|
+
...flags
|
|
47
51
|
].filter((name) => name !== void 0);
|
|
48
52
|
};
|
|
49
53
|
/** The fields to label the list's rows for: the table's columns and every display's, so one load serves them all. */
|
package/dist/app/field-editor.js
CHANGED
|
@@ -90,6 +90,10 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
90
90
|
className: "text-[13px]",
|
|
91
91
|
children: hints.prefix
|
|
92
92
|
}) : void 0,
|
|
93
|
+
trailing: hints?.suffix ? /* @__PURE__ */ jsx("span", {
|
|
94
|
+
className: "text-[13px]",
|
|
95
|
+
children: hints.suffix.trim()
|
|
96
|
+
}) : void 0,
|
|
93
97
|
placeholder,
|
|
94
98
|
className: cn(numeric.has(field.type) && "text-right tabular-nums"),
|
|
95
99
|
value: String(value),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FileText, RulerDimensionLine, Send, Wallet } from "lucide-react";
|
|
2
|
+
//#region src/app/named-icon.ts
|
|
3
|
+
/** The icons an action's or a field's `icon` may name. */
|
|
4
|
+
const byName = {
|
|
5
|
+
wallet: Wallet,
|
|
6
|
+
"file-text": FileText,
|
|
7
|
+
send: Send,
|
|
8
|
+
ruler: RulerDimensionLine
|
|
9
|
+
};
|
|
10
|
+
/** The icon a name stands for, or nothing for a name the app does not know. */
|
|
11
|
+
const namedIcon = (name) => name !== void 0 && Object.hasOwn(byName, name) ? byName[name] : void 0;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { namedIcon };
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { cn } from "../../lib/cn.js";
|
|
2
|
-
import { Badge } from "../../primitives/badge.js";
|
|
3
2
|
import { keys } from "../../data/query-keys.js";
|
|
4
3
|
import { useClient } from "../../data/api-provider.js";
|
|
4
|
+
import { fetchLabels } from "../../live/relation-labels.js";
|
|
5
5
|
import { useAdminMeta } from "../meta-gate.js";
|
|
6
6
|
import { AppLink } from "../use-app-address.js";
|
|
7
7
|
import { fileShown } from "../../live/file-values.js";
|
|
8
8
|
import { displayValue } from "../../live/display-value.js";
|
|
9
9
|
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
10
10
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
11
|
-
import {
|
|
12
|
-
import { fetchLabels } from "../../live/relation-labels.js";
|
|
11
|
+
import { ValueBadge } from "../value-badge.js";
|
|
13
12
|
import { jsx } from "react/jsx-runtime";
|
|
14
13
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
15
14
|
//#region src/app/pages/field-value.tsx
|
|
@@ -59,8 +58,9 @@ const BuiltInValue = ({ model, field, value, format, relationLabel }) => {
|
|
|
59
58
|
children: file.name
|
|
60
59
|
});
|
|
61
60
|
const text = displayValue(field, value, hints);
|
|
62
|
-
if (field.type === "enum" || hints.format === "badge") return /* @__PURE__ */ jsx(
|
|
63
|
-
|
|
61
|
+
if (field.type === "enum" || hints.format === "badge") return /* @__PURE__ */ jsx(ValueBadge, {
|
|
62
|
+
value,
|
|
63
|
+
hints,
|
|
64
64
|
children: text
|
|
65
65
|
});
|
|
66
66
|
return /* @__PURE__ */ jsx("span", {
|
|
@@ -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. */
|
package/dist/app/record-page.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Badge } from "../primitives/badge.js";
|
|
2
1
|
import { useAuth } from "../auth/auth-provider.js";
|
|
3
2
|
import { formatRelativeTime } from "../format/date.js";
|
|
4
3
|
import { Button } from "../primitives/button.js";
|
|
@@ -24,6 +23,7 @@ import { RecordHeader } from "../record-view/record-header.js";
|
|
|
24
23
|
import { toneFor } from "../live/columns-from-model.js";
|
|
25
24
|
import { useDeleteFlow } from "./delete/use-delete-flow.js";
|
|
26
25
|
import { useRestore } from "./delete/use-restore.js";
|
|
26
|
+
import { ValueBadge } from "./value-badge.js";
|
|
27
27
|
import { auditField } from "../live/audit-fields.js";
|
|
28
28
|
import { CustomSection } from "./custom-section.js";
|
|
29
29
|
import { RecordActions } from "./pages/record-actions.js";
|
|
@@ -176,8 +176,9 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
|
|
|
176
176
|
const field = model.fields[name];
|
|
177
177
|
if (!field || !visible(name)) return [];
|
|
178
178
|
const text = displayValue(field, stored.record[name], hint(name));
|
|
179
|
-
const builtIn = field.type === "enum" ? /* @__PURE__ */ jsx(
|
|
180
|
-
|
|
179
|
+
const builtIn = field.type === "enum" ? /* @__PURE__ */ jsx(ValueBadge, {
|
|
180
|
+
value: stored.record[name],
|
|
181
|
+
hints: hint(name),
|
|
181
182
|
children: text
|
|
182
183
|
}) : /* @__PURE__ */ jsx("strong", { children: text });
|
|
183
184
|
const shown = shownWidget(fieldTypes, field, hint(name));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Badge } from "../primitives/badge.js";
|
|
2
|
+
import { valueColor } from "../live/value-color.js";
|
|
3
|
+
import { toneFor } from "../live/columns-from-model.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/app/value-badge.tsx
|
|
6
|
+
/** A stored value as a badge: filled with the colour the view gives it and in its badge shape, else in its tone. */
|
|
7
|
+
const ValueBadge = ({ value, hints, children }) => /* @__PURE__ */ jsx(Badge, {
|
|
8
|
+
tone: toneFor(String(value)),
|
|
9
|
+
color: valueColor(hints?.valueColors, value),
|
|
10
|
+
shape: hints?.badgeShape,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ValueBadge };
|
|
@@ -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
|