@protobase/ui 0.21.3 → 0.21.5
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 +13 -1
- package/dist/app/delete/blocked-delete.js +34 -0
- package/dist/app/delete/use-delete-flow.js +20 -2
- package/dist/app/displays/list-cards.js +2 -2
- package/dist/app/document-title.js +27 -0
- package/dist/app/embedded-list/embedded-list.js +10 -1
- package/dist/app/field-editor.js +2 -5
- package/dist/app/hand-order/hand-sorted-list.js +10 -1
- package/dist/app/list-empty-state.js +31 -0
- package/dist/app/list-empty.js +23 -0
- package/dist/app/list-page.js +28 -3
- package/dist/app/numeric-input.js +21 -0
- package/dist/data/meta-data.d.ts +2 -0
- package/dist/data/use-meta.js +1 -0
- package/dist/data-grid/data-grid.js +10 -2
- package/dist/data-grid/grid-row.js +5 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BmBlR0fJ.js +69 -0
- package/dist/shell/assets/{index-RHKnIOOP.js → index-BMiDZRGI.js} +1 -1
- package/dist/shell/assets/{maplibre-DN_v_fcJ.js → maplibre-BFMEpd4m.js} +1 -1
- package/dist/shell/assets/{markdown-content-Be4vIiWm.js → markdown-content-yqq0wmAS.js} +1 -1
- package/dist/shell/assets/{record-map-CVeshts3.js → record-map-DhpikXZs.js} +1 -1
- package/dist/shell/assets/{records-map-ClnWq-RH.js → records-map-LDeVJQab.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DWCBNg3c.js → shared-protobase-ui-DfPkkHCn.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-wZRscg1o.js +0 -69
package/dist/app/app.js
CHANGED
|
@@ -21,6 +21,7 @@ import { useProjectUi } from "./pages/project-ui.js";
|
|
|
21
21
|
import { AssistantButton, AssistantPanel } from "./assistant-shell.js";
|
|
22
22
|
import { bottomBarFromMeta } from "./bottom-bar-from-meta.js";
|
|
23
23
|
import { breadcrumbFor } from "./breadcrumb-for.js";
|
|
24
|
+
import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
|
|
24
25
|
import { sourceStatusOf } from "../data/with-sources.js";
|
|
25
26
|
import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
|
|
26
27
|
import { useAppAddress } from "./use-app-address.js";
|
|
@@ -128,15 +129,26 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
128
129
|
const outsideOrganizations = Boolean(organizationSettings) && current !== void 0 && !current.organization;
|
|
129
130
|
const organizationPage = organizationSettings && user ? organizationPageTitles[path] : void 0;
|
|
130
131
|
const accountPage = !user ? void 0 : path === "/-/account" ? "Sign-in & security" : path === "/-/sign-in-policy" ? "Sign-in policy" : organizationPage;
|
|
132
|
+
const resourceLabel = view?.names?.plural ?? (resource && humanize(resource));
|
|
131
133
|
const breadcrumb = accountPage ? [accountPage] : breadcrumbFor({
|
|
132
134
|
basePath,
|
|
133
135
|
address,
|
|
134
136
|
group: group?.label,
|
|
135
137
|
page,
|
|
136
|
-
resourceLabel
|
|
138
|
+
resourceLabel,
|
|
137
139
|
recordTitle: title,
|
|
138
140
|
recordLoading: titleLoading
|
|
139
141
|
});
|
|
142
|
+
const singular = view?.names?.singular ?? (resource && humanize(resource));
|
|
143
|
+
useDocumentTitle(documentTitle(titleParts({
|
|
144
|
+
address,
|
|
145
|
+
page,
|
|
146
|
+
accountPage,
|
|
147
|
+
resourceLabel,
|
|
148
|
+
singular,
|
|
149
|
+
recordTitle: title,
|
|
150
|
+
recordLoading: titleLoading
|
|
151
|
+
}), meta.appName));
|
|
140
152
|
const content = user && path === "/-/account" ? /* @__PURE__ */ jsx(AccountSecurityPage, { email: user.email }) : user && path === "/-/sign-in-policy" ? admin ? /* @__PURE__ */ jsx(SignInPolicyPage, {}) : /* @__PURE__ */ jsx(Notice, {
|
|
141
153
|
title: "Admins only",
|
|
142
154
|
children: "Only an admin can change how people sign in."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { humanize } from "../../live/naming.js";
|
|
2
|
+
import { createWhere, printFilter } from "@protobase/schema";
|
|
3
|
+
//#region src/app/delete/blocked-delete.ts
|
|
4
|
+
const where$1 = createWhere();
|
|
5
|
+
/** What a delete other records still point at says, such as "Orders still point at this company." */
|
|
6
|
+
const blockedDeleteText = ({ blockingPlural, noun }) => `${blockingPlural} still point at this ${noun.toLowerCase()}.`;
|
|
7
|
+
/**
|
|
8
|
+
* Why the delete was refused, naming the records that point at it by their view's plural, and, when the server names
|
|
9
|
+
* the field pointing at it, a link to their list filtered to those records. `undefined` for a resource the app does not show.
|
|
10
|
+
*/
|
|
11
|
+
const blockedDelete = ({ blockedBy, resources, views, noun, key }) => {
|
|
12
|
+
const { resource, field } = blockedBy;
|
|
13
|
+
if (!resources[resource]) return void 0;
|
|
14
|
+
const plural = views[resource]?.names?.plural ?? humanize(resource);
|
|
15
|
+
const text = blockedDeleteText({
|
|
16
|
+
blockingPlural: plural,
|
|
17
|
+
noun
|
|
18
|
+
});
|
|
19
|
+
if (!field || key === void 0) return { text };
|
|
20
|
+
const list = {
|
|
21
|
+
kind: "list",
|
|
22
|
+
resource,
|
|
23
|
+
filter: printFilter(where$1.eq(field, key))
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
text,
|
|
27
|
+
link: {
|
|
28
|
+
label: `Show ${plural.toLowerCase()}`,
|
|
29
|
+
to: list
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { blockedDelete, blockedDeleteText };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useClient } from "../../data/api-provider.js";
|
|
2
2
|
import { humanize } from "../../live/naming.js";
|
|
3
3
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
4
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
5
|
+
import { useAppAddress } from "../use-app-address.js";
|
|
6
|
+
import { blockedDelete } from "./blocked-delete.js";
|
|
4
7
|
import { BulkDeleteDialog, ConfirmDeleteDialog, ConflictDialog } from "./dialogs.js";
|
|
5
8
|
import { diffRecords } from "./record-diff.js";
|
|
6
9
|
import { useState } from "react";
|
|
@@ -21,6 +24,15 @@ const useDeleteFlow = ({ model, view, onDeleted, onReview, restore }) => {
|
|
|
21
24
|
const [busy, setBusy] = useState(false);
|
|
22
25
|
const noun = view?.names?.singular ?? humanize(model.name);
|
|
23
26
|
const plural = view?.names?.plural ?? humanize(model.name);
|
|
27
|
+
const { resources, views } = useAdminMeta();
|
|
28
|
+
const { href, open } = useAppAddress();
|
|
29
|
+
const blocked = (error, target) => error.blockedBy && blockedDelete({
|
|
30
|
+
blockedBy: error.blockedBy,
|
|
31
|
+
resources,
|
|
32
|
+
views,
|
|
33
|
+
noun,
|
|
34
|
+
...model.primaryKey.length === 1 && { key: target.key }
|
|
35
|
+
});
|
|
24
36
|
const refresh = () => {
|
|
25
37
|
for (const prefix of [
|
|
26
38
|
"page",
|
|
@@ -87,10 +99,16 @@ const useDeleteFlow = ({ model, view, onDeleted, onReview, restore }) => {
|
|
|
87
99
|
} catch (error) {
|
|
88
100
|
if (!isApiError(error)) throw error;
|
|
89
101
|
setFlow(void 0);
|
|
102
|
+
const why = blocked(error, target);
|
|
90
103
|
toast.show({
|
|
91
104
|
state: "error",
|
|
92
105
|
title: `Could not delete ${target.title}`,
|
|
93
|
-
description: error.message
|
|
106
|
+
description: why?.text ?? error.message,
|
|
107
|
+
...why?.link && { action: {
|
|
108
|
+
label: why.link.label,
|
|
109
|
+
href: href(why.link.to),
|
|
110
|
+
onNavigate: open
|
|
111
|
+
} }
|
|
94
112
|
});
|
|
95
113
|
} finally {
|
|
96
114
|
setBusy(false);
|
|
@@ -106,7 +124,7 @@ const useDeleteFlow = ({ model, view, onDeleted, onReview, restore }) => {
|
|
|
106
124
|
if (!isApiError(error)) throw error;
|
|
107
125
|
return {
|
|
108
126
|
title: target.title,
|
|
109
|
-
message: error.message
|
|
127
|
+
message: blocked(error, target)?.text ?? error.message
|
|
110
128
|
};
|
|
111
129
|
}
|
|
112
130
|
};
|
|
@@ -13,7 +13,7 @@ const plainHeight = 44;
|
|
|
13
13
|
* The list page's cards display: a card per record (see `RecordCard`), scrolled like the table. An embedded list,
|
|
14
14
|
* which shows a few cards whole, draws them with `CardsDisplay`.
|
|
15
15
|
*/
|
|
16
|
-
const ListCards = ({ model, view, display, list }) => {
|
|
16
|
+
const ListCards = ({ model, view, display, list, empty }) => {
|
|
17
17
|
const plural = view?.names?.plural ?? humanize(model.name);
|
|
18
18
|
const pictured = display.card === void 0 && (display.image !== void 0 || display.photos !== void 0);
|
|
19
19
|
if (list.isLoading) return /* @__PURE__ */ jsx("div", {
|
|
@@ -24,7 +24,7 @@ const ListCards = ({ model, view, display, list }) => {
|
|
|
24
24
|
title: "Could not load rows",
|
|
25
25
|
error: list.error
|
|
26
26
|
});
|
|
27
|
-
if (!list.source || list.source.total === 0) return /* @__PURE__ */ jsxs("p", {
|
|
27
|
+
if (!list.source || list.source.total === 0) return empty ?? /* @__PURE__ */ jsxs("p", {
|
|
28
28
|
className: "py-8 text-center text-muted-foreground",
|
|
29
29
|
children: [
|
|
30
30
|
"No ",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
//#region src/app/document-title.ts
|
|
3
|
+
/** What a tab names, most specific first: the record and its list, the new record and its list, the list, or the page. */
|
|
4
|
+
const titleParts = ({ address, page, accountPage, resourceLabel, singular, recordTitle, recordLoading }) => {
|
|
5
|
+
if (accountPage) return [accountPage];
|
|
6
|
+
if (page) return [page.title];
|
|
7
|
+
if (!address || address.kind === "page") return [];
|
|
8
|
+
const list = resourceLabel ? [resourceLabel] : [];
|
|
9
|
+
if (address.kind === "list") return list;
|
|
10
|
+
if (address.kind === "create") return [...singular ? [`New ${singular.toLowerCase()}`] : [], ...list];
|
|
11
|
+
const record = recordTitle ?? (recordLoading ? void 0 : address.key);
|
|
12
|
+
return [...record ? [record] : [], ...list];
|
|
13
|
+
};
|
|
14
|
+
/** A tab's title: its parts, then the app's name, as in "Nordlicht GmbH · Companies · Protobase". */
|
|
15
|
+
const documentTitle = (parts, appName) => [...parts.filter(Boolean), appName].join(" · ");
|
|
16
|
+
/** Shows `text` as the tab's title while mounted, then puts back the title it found. */
|
|
17
|
+
const useDocumentTitle = (text) => {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const before = document.title;
|
|
20
|
+
document.title = text;
|
|
21
|
+
return () => {
|
|
22
|
+
document.title = before;
|
|
23
|
+
};
|
|
24
|
+
}, [text]);
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { documentTitle, titleParts, useDocumentTitle };
|
|
@@ -40,7 +40,7 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
|
|
|
40
40
|
const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll, link = true }) => {
|
|
41
41
|
const { resources, views } = useAdminMeta();
|
|
42
42
|
const { fields: fieldTypes } = useProjectUi();
|
|
43
|
-
const { go } = useAppAddress();
|
|
43
|
+
const { go, href } = useAppAddress();
|
|
44
44
|
const xs = useIsXs();
|
|
45
45
|
const [orderBy, setOrderBy] = useState(sort || defaultOrderBy(model, view));
|
|
46
46
|
const names = columns ?? listColumnNames(model, view);
|
|
@@ -125,6 +125,14 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
|
|
|
125
125
|
resource: model.name,
|
|
126
126
|
key: recordKey(model, row)
|
|
127
127
|
});
|
|
128
|
+
const recordLink = (row) => ({
|
|
129
|
+
href: href({
|
|
130
|
+
kind: "record",
|
|
131
|
+
resource: model.name,
|
|
132
|
+
key: recordKey(model, row)
|
|
133
|
+
}),
|
|
134
|
+
onFollow: () => openRecord(row)
|
|
135
|
+
});
|
|
128
136
|
const singular = view?.names?.singular ?? humanize(model.name);
|
|
129
137
|
return /* @__PURE__ */ jsxs("div", {
|
|
130
138
|
className: "flex flex-col gap-3",
|
|
@@ -136,6 +144,7 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
|
|
|
136
144
|
sort: parseSort(orderBy),
|
|
137
145
|
onSortChange: (next) => setOrderBy(sortToOrderBy(next)),
|
|
138
146
|
onOpenRow: openRecord,
|
|
147
|
+
rowLink: recordLink,
|
|
139
148
|
rowActions: [{
|
|
140
149
|
label: `Open ${singular.toLowerCase()}`,
|
|
141
150
|
onSelect: openRecord
|
package/dist/app/field-editor.js
CHANGED
|
@@ -16,15 +16,11 @@ import { useShownWidget } from "./field-types/shown-widget.js";
|
|
|
16
16
|
import { WidgetPart } from "./field-types/widget-part.js";
|
|
17
17
|
import { Markdown } from "../markdown/markdown.js";
|
|
18
18
|
import { MarkdownEditor } from "../markdown/markdown-editor.js";
|
|
19
|
+
import { numeric, numericInputProps } from "./numeric-input.js";
|
|
19
20
|
import { ArrowUpRight } from "lucide-react";
|
|
20
21
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
21
22
|
import { enumLabel } from "@protobase/schema";
|
|
22
23
|
//#region src/app/field-editor.tsx
|
|
23
|
-
const numeric = /* @__PURE__ */ new Set([
|
|
24
|
-
"integer",
|
|
25
|
-
"bigint",
|
|
26
|
-
"decimal"
|
|
27
|
-
]);
|
|
28
24
|
const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo, relationPicker, invalid, locked, placeholder, reveal, resource, onChange }) => {
|
|
29
25
|
const control = useFieldControl();
|
|
30
26
|
if (field.type === "file") return /* @__PURE__ */ jsx(FileField, {
|
|
@@ -136,6 +132,7 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
136
132
|
trailing: hints?.suffix ? /* @__PURE__ */ jsx("span", { children: hints.suffix.trim() }) : void 0,
|
|
137
133
|
placeholder,
|
|
138
134
|
className: cn(numeric.has(field.type) && "text-right tabular-nums"),
|
|
135
|
+
...numericInputProps(field.type),
|
|
139
136
|
value: String(value),
|
|
140
137
|
onChange: (event) => onChange(event.target.value)
|
|
141
138
|
});
|
|
@@ -20,7 +20,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
const HandSortedList = ({ model, view, field, filter, columns }) => {
|
|
21
21
|
const { resources, views } = useAdminMeta();
|
|
22
22
|
const { fields: fieldTypes } = useProjectUi();
|
|
23
|
-
const { go } = useAppAddress();
|
|
23
|
+
const { go, href } = useAppAddress();
|
|
24
24
|
const specs = useMemo(() => embeddedColumns(model, view, columns, {
|
|
25
25
|
resources,
|
|
26
26
|
views,
|
|
@@ -80,6 +80,14 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
|
|
|
80
80
|
resource: model.name,
|
|
81
81
|
key: recordKey(model, row)
|
|
82
82
|
});
|
|
83
|
+
const recordLink = (row) => ({
|
|
84
|
+
href: href({
|
|
85
|
+
kind: "record",
|
|
86
|
+
resource: model.name,
|
|
87
|
+
key: recordKey(model, row)
|
|
88
|
+
}),
|
|
89
|
+
onFollow: () => openRecord(row)
|
|
90
|
+
});
|
|
83
91
|
return /* @__PURE__ */ jsx(DataGrid, {
|
|
84
92
|
fit: true,
|
|
85
93
|
scroll: "parent",
|
|
@@ -87,6 +95,7 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
|
|
|
87
95
|
source,
|
|
88
96
|
latencyMs: 0,
|
|
89
97
|
onOpenRow: openRecord,
|
|
98
|
+
rowLink: recordLink,
|
|
90
99
|
rowNumbers: false,
|
|
91
100
|
reorder: {
|
|
92
101
|
label: (row) => recordTitle(model, view, row, row.id),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/app/list-empty-state.ts
|
|
2
|
+
/** Why a list has no rows, and the buttons that help: clear the search, clear the filters, or create the first record. */
|
|
3
|
+
const listEmptyState = ({ plural, search, filtered, scoped, canCreate }) => {
|
|
4
|
+
const noun = plural.toLowerCase();
|
|
5
|
+
if (search) return {
|
|
6
|
+
message: `No ${noun} match “${search}”.`,
|
|
7
|
+
clearSearch: true,
|
|
8
|
+
clearFilters: filtered,
|
|
9
|
+
create: false
|
|
10
|
+
};
|
|
11
|
+
if (filtered) return {
|
|
12
|
+
message: `No ${noun} match these filters.`,
|
|
13
|
+
clearSearch: false,
|
|
14
|
+
clearFilters: true,
|
|
15
|
+
create: false
|
|
16
|
+
};
|
|
17
|
+
if (scoped) return {
|
|
18
|
+
message: `No ${noun} match.`,
|
|
19
|
+
clearSearch: false,
|
|
20
|
+
clearFilters: false,
|
|
21
|
+
create: false
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
message: `No ${noun} yet.`,
|
|
25
|
+
clearSearch: false,
|
|
26
|
+
clearFilters: false,
|
|
27
|
+
create: canCreate
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { listEmptyState };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/app/list-empty.tsx
|
|
4
|
+
/** A list without rows: why (see `listEmptyState`), with buttons to clear its search or filters, or its New button. */
|
|
5
|
+
const ListEmpty = ({ state, onClearSearch, onClearFilters, create }) => /* @__PURE__ */ jsxs("div", {
|
|
6
|
+
className: "flex flex-col items-center gap-3 py-8 text-center text-muted-foreground",
|
|
7
|
+
children: [/* @__PURE__ */ jsx("p", { children: state.message }), (state.clearSearch || state.clearFilters || state.create && create) && /* @__PURE__ */ jsxs("div", {
|
|
8
|
+
className: "flex flex-wrap justify-center gap-2",
|
|
9
|
+
children: [
|
|
10
|
+
state.clearSearch && /* @__PURE__ */ jsx(Button, {
|
|
11
|
+
onClick: onClearSearch,
|
|
12
|
+
children: "Clear search"
|
|
13
|
+
}),
|
|
14
|
+
state.clearFilters && /* @__PURE__ */ jsx(Button, {
|
|
15
|
+
onClick: onClearFilters,
|
|
16
|
+
children: "Clear filters"
|
|
17
|
+
}),
|
|
18
|
+
state.create && create
|
|
19
|
+
]
|
|
20
|
+
})]
|
|
21
|
+
});
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ListEmpty };
|
package/dist/app/list-page.js
CHANGED
|
@@ -42,6 +42,8 @@ import { createOffered } from "./create/create-offered.js";
|
|
|
42
42
|
import { ImportButton } from "./import/import-button.js";
|
|
43
43
|
import { exportOffered, exportsSelection } from "./export/export-options.js";
|
|
44
44
|
import { useExportFlow } from "./export/use-export-flow.js";
|
|
45
|
+
import { ListEmpty } from "./list-empty.js";
|
|
46
|
+
import { listEmptyState } from "./list-empty-state.js";
|
|
45
47
|
import { ListCards } from "./displays/list-cards.js";
|
|
46
48
|
import { useListScroll } from "./list-scroll.js";
|
|
47
49
|
import { ListChart } from "./list-chart.js";
|
|
@@ -113,7 +115,7 @@ const ListPage = ({ address }) => {
|
|
|
113
115
|
};
|
|
114
116
|
const LoadedList = ({ model, address, scope, heading }) => {
|
|
115
117
|
const meta = useAdminMeta();
|
|
116
|
-
const { set, go } = useAppAddress();
|
|
118
|
+
const { set, go, href } = useAppAddress();
|
|
117
119
|
const resource = model.name;
|
|
118
120
|
const view = meta.views[resource];
|
|
119
121
|
const fields = useMemo(() => widgetFields(view, model), [view, model]);
|
|
@@ -336,12 +338,21 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
336
338
|
key: recordKey(model, row)
|
|
337
339
|
});
|
|
338
340
|
};
|
|
341
|
+
const recordLink = (row) => ({
|
|
342
|
+
href: href({
|
|
343
|
+
kind: "record",
|
|
344
|
+
resource,
|
|
345
|
+
key: recordKey(model, row)
|
|
346
|
+
}),
|
|
347
|
+
onFollow: () => openRecord(row)
|
|
348
|
+
});
|
|
339
349
|
const table = rows.source ? /* @__PURE__ */ jsx(DataGrid, {
|
|
340
350
|
columns,
|
|
341
351
|
source: rows.source,
|
|
342
352
|
sort: parseSort(orderBy),
|
|
343
353
|
onSortChange: (sort) => set({ orderBy: sortToOrderBy(sort) }),
|
|
344
354
|
onOpenRow: openRecord,
|
|
355
|
+
rowLink: recordLink,
|
|
345
356
|
cellEditor: cellEditing.editor,
|
|
346
357
|
rowPatches: cellEditing.patches,
|
|
347
358
|
initialScrollIndex: scroll.initialIndex,
|
|
@@ -373,13 +384,27 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
373
384
|
initialIndex: scroll.initialIndex,
|
|
374
385
|
onScrollIndex: scroll.remember
|
|
375
386
|
};
|
|
387
|
+
const emptyState = listEmptyState({
|
|
388
|
+
plural,
|
|
389
|
+
search: search?.kind === "search" ? search.text : "",
|
|
390
|
+
filtered: chips.length > 0,
|
|
391
|
+
scoped: Boolean(scope) || extras.some((clause) => clause.kind !== "search"),
|
|
392
|
+
canCreate: Boolean(createButton)
|
|
393
|
+
});
|
|
394
|
+
const empty = /* @__PURE__ */ jsx(ListEmpty, {
|
|
395
|
+
state: emptyState,
|
|
396
|
+
onClearSearch: () => commitSearch(""),
|
|
397
|
+
onClearFilters: () => setFilter(emptyFilterState),
|
|
398
|
+
create: createButton
|
|
399
|
+
});
|
|
376
400
|
const drawDisplay = () => {
|
|
377
|
-
if (!display) return table;
|
|
401
|
+
if (!display) return rows.source?.total === 0 && !rows.error ? empty : table;
|
|
378
402
|
if (display.kind === "cards") return /* @__PURE__ */ jsx(ListCards, {
|
|
379
403
|
model,
|
|
380
404
|
view,
|
|
381
405
|
display,
|
|
382
|
-
list: cardsList
|
|
406
|
+
list: cardsList,
|
|
407
|
+
empty
|
|
383
408
|
}, rows.key.join(" "));
|
|
384
409
|
if (display.kind === "map") return /* @__PURE__ */ jsx(MapDisplay, {
|
|
385
410
|
model,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/app/numeric-input.ts
|
|
2
|
+
/** Field types whose values are numbers: right-aligned, with a number keyboard on phones. */
|
|
3
|
+
const numeric = /* @__PURE__ */ new Set([
|
|
4
|
+
"integer",
|
|
5
|
+
"bigint",
|
|
6
|
+
"decimal"
|
|
7
|
+
]);
|
|
8
|
+
/** The keyboard a phone shows for a field: a number pad for whole numbers, one with a decimal key for decimals. Enter saves or does nothing, hence `done`. */
|
|
9
|
+
const numericInputProps = (type) => {
|
|
10
|
+
if (type === "integer" || type === "bigint") return {
|
|
11
|
+
inputMode: "numeric",
|
|
12
|
+
enterKeyHint: "done"
|
|
13
|
+
};
|
|
14
|
+
if (type === "decimal") return {
|
|
15
|
+
inputMode: "decimal",
|
|
16
|
+
enterKeyHint: "done"
|
|
17
|
+
};
|
|
18
|
+
return {};
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { numeric, numericInputProps };
|
package/dist/data/meta-data.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { PageModel } from "@protobase/layout";
|
|
|
4
4
|
//#region src/data/meta-data.d.ts
|
|
5
5
|
export type MetaData = {
|
|
6
6
|
etag: string;
|
|
7
|
+
/** The app's name, which ends the tab titles. */
|
|
8
|
+
appName: string;
|
|
7
9
|
resources: Record<string, ResourceModel>;
|
|
8
10
|
views: Record<string, ViewModel>;
|
|
9
11
|
/** Composed pages by name. */
|
package/dist/data/use-meta.js
CHANGED
|
@@ -26,6 +26,7 @@ const useMeta = () => {
|
|
|
26
26
|
}
|
|
27
27
|
return {
|
|
28
28
|
etag: result.etag,
|
|
29
|
+
appName: result.meta.appName,
|
|
29
30
|
resources: Object.fromEntries(result.meta.resources.map((model) => [model.name, model])),
|
|
30
31
|
views: Object.fromEntries(result.meta.views.map((model) => [model.resource, model])),
|
|
31
32
|
pages: Object.fromEntries((result.meta.pages ?? []).map((model) => [model.name, model])),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
3
|
+
import { RouteLink } from "../primitives/route-link.js";
|
|
3
4
|
import { buildColumns, features } from "./columns.js";
|
|
4
5
|
import { GridFooter } from "./grid-footer.js";
|
|
5
6
|
import { RowMenu, availableActions } from "./row-menu.js";
|
|
@@ -19,7 +20,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
19
20
|
import { useTable } from "@tanstack/react-table";
|
|
20
21
|
//#region src/data-grid/data-grid.tsx
|
|
21
22
|
const headerHeight = 32;
|
|
22
|
-
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, cellEditor, rowPatches, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable: selecting = true, reorder, fit = false, scroll = "own", className }) => {
|
|
23
|
+
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowLink, rowActions, cellEditor, rowPatches, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable: selecting = true, reorder, fit = false, scroll = "own", className }) => {
|
|
23
24
|
const selectable = selecting && !reorder;
|
|
24
25
|
const rowHeight = compact ? 28 : 36;
|
|
25
26
|
const narrow = !useIsDesktop();
|
|
@@ -150,6 +151,7 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
150
151
|
onOpenRow,
|
|
151
152
|
container: gridRef
|
|
152
153
|
});
|
|
154
|
+
const linkColumn = (columns.find((spec) => spec.title) ?? columns[0])?.id;
|
|
153
155
|
const minWidth = gridMinWidth(sized, gutter, trailing, minScale);
|
|
154
156
|
const own = scroll === "own";
|
|
155
157
|
const header = /* @__PURE__ */ jsx("div", {
|
|
@@ -201,9 +203,15 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
201
203
|
}) : void 0,
|
|
202
204
|
cells: tableRow.getAllCells().map((cell) => {
|
|
203
205
|
const content = /* @__PURE__ */ jsx(table.FlexRender, { cell });
|
|
206
|
+
const editable = Boolean(cellEditor?.editable(row, cell.column.id));
|
|
207
|
+
const linked = rowLink && cell.column.id === linkColumn && !editable ? /* @__PURE__ */ jsx(RouteLink, {
|
|
208
|
+
to: rowLink(row),
|
|
209
|
+
className: "flex min-w-0 items-center hover:underline",
|
|
210
|
+
children: content
|
|
211
|
+
}) : content;
|
|
204
212
|
return {
|
|
205
213
|
spec: columns.find((spec) => spec.id === cell.column.id),
|
|
206
|
-
...cellEditing ? cellEditing.cell(index, row, cell.column.id,
|
|
214
|
+
...cellEditing ? cellEditing.cell(index, row, cell.column.id, linked) : { content: linked }
|
|
207
215
|
};
|
|
208
216
|
})
|
|
209
217
|
};
|
|
@@ -40,10 +40,14 @@ const dragStyle = (frame, dragY, transition) => ({
|
|
|
40
40
|
transform: dragY === 0 ? void 0 : `translateY(${dragY}px)`,
|
|
41
41
|
transition
|
|
42
42
|
});
|
|
43
|
+
const openUnlessLink = (onOpen) => (event) => {
|
|
44
|
+
if (event.target.closest("a")) return;
|
|
45
|
+
onOpen();
|
|
46
|
+
};
|
|
43
47
|
const DataRow = ({ cells, selected, onToggle, onOpen, menu, handle, rowRef, dragY, transition, dragging, ...frame }) => /* @__PURE__ */ jsxs("div", {
|
|
44
48
|
ref: rowRef,
|
|
45
49
|
role: "row",
|
|
46
|
-
onClick: onOpen,
|
|
50
|
+
onClick: onOpen && openUnlessLink(onOpen),
|
|
47
51
|
className: cn("absolute inset-x-0 top-0 grid items-center border-b", selected ? "bg-primary-soft" : "bg-background hover:bg-surface", onOpen && "cursor-pointer", dragging && "z-30 bg-surface shadow-pop"),
|
|
48
52
|
style: dragY === void 0 ? frameStyle(frame) : dragStyle(frame, dragY, transition),
|
|
49
53
|
children: [
|
package/dist/package.js
CHANGED