@protobase/ui 0.22.9 → 0.22.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/current-address.d.ts +1 -0
- package/dist/app/current-address.js +2 -1
- package/dist/app/record-mode/shown-layout.js +16 -10
- package/dist/app/record-mode/use-shown-layout.js +2 -2
- package/dist/app/record-page.js +39 -14
- package/dist/app/record-tabs.js +31 -0
- package/dist/app/use-focus-first-invalid.js +10 -3
- package/dist/package.js +1 -1
- package/dist/record-view/record-tabs.js +22 -0
- package/dist/shell/assets/app-CaGtNVPj.js +69 -0
- package/dist/shell/assets/{index-DMNlGow_.js → index-9GDE3q4_.js} +1 -1
- package/dist/shell/assets/{maplibre-Cm6vGs92.js → maplibre-BZ8P8gVB.js} +1 -1
- package/dist/shell/assets/{markdown-content-DuCUZXHs.js → markdown-content-CXq2IuhT.js} +1 -1
- package/dist/shell/assets/{record-map-Cj9snUMw.js → record-map-BmYvqd4a.js} +1 -1
- package/dist/shell/assets/{records-map-ByEdl0IP.js → records-map-D8odC3U5.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DaruuG7b.js → shared-protobase-ui-aDCxSepP.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-B6FpXlpX.js +0 -69
|
@@ -35,7 +35,8 @@ const currentAddress = (read, defaults) => {
|
|
|
35
35
|
resource: read.resource,
|
|
36
36
|
key: encodeKey(read.key),
|
|
37
37
|
...read.edit && { edit: true },
|
|
38
|
-
...read.open && { open: read.open }
|
|
38
|
+
...read.open && { open: read.open },
|
|
39
|
+
...read.tab && { tab: read.tab }
|
|
39
40
|
};
|
|
40
41
|
if (read?.kind !== "list") return read;
|
|
41
42
|
return {
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { whenShown } from "./record-mode.js";
|
|
2
2
|
import { isRecordCondition } from "@protobase/schema";
|
|
3
3
|
//#region src/app/record-mode/shown-layout.ts
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
const innerItems = (item) => item.kind === "group" || item.kind === "tab" ? item.items : [];
|
|
5
|
+
const whenOf = (item) => item.kind === "tab" ? void 0 : item.when;
|
|
6
|
+
/** The items with a record condition in `when`, those inside tabs and groups too, whose conditions the page evaluates: a group before its items. */
|
|
7
|
+
const conditionalItems = (items) => items.flatMap((item) => [item, ...conditionalItems(innerItems(item))]).filter((item) => isRecordCondition(whenOf(item)));
|
|
6
8
|
/**
|
|
7
9
|
* The items that show: those with a record condition only when it holds (`held`, one answer per conditional item in
|
|
8
10
|
* the order `conditionalItems` lists them, `undefined` while loading) and the page is no preview; the others by the
|
|
9
|
-
* form or read view. A group keeps the items inside it that show, and goes when it or all of them do not.
|
|
11
|
+
* form or read view. A tab or group keeps the items inside it that show, and goes when it or all of them do not.
|
|
10
12
|
*/
|
|
11
13
|
const shownLayout = (items, held, editing, preview) => {
|
|
12
14
|
let next = 0;
|
|
13
|
-
const shown = (item) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const shown = (item) => {
|
|
16
|
+
const when = whenOf(item);
|
|
17
|
+
return isRecordCondition(when) ? !preview && held[next++] === true : whenShown(when, editing);
|
|
18
|
+
};
|
|
19
|
+
const keep = (list) => list.flatMap((item) => {
|
|
20
|
+
const itself = shown(item);
|
|
21
|
+
if (item.kind !== "group" && item.kind !== "tab") return itself ? [item] : [];
|
|
22
|
+
const inner = keep(item.items);
|
|
23
|
+
return itself && inner.length > 0 ? [{
|
|
19
24
|
...item,
|
|
20
25
|
items: inner
|
|
21
26
|
}] : [];
|
|
22
27
|
});
|
|
28
|
+
return keep(items);
|
|
23
29
|
};
|
|
24
30
|
//#endregion
|
|
25
|
-
export { conditionalItems, shownLayout };
|
|
31
|
+
export { conditionalItems, shownLayout, whenOf };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useConditions } from "../pages/use-conditions.js";
|
|
2
|
-
import { conditionalItems, shownLayout } from "./shown-layout.js";
|
|
2
|
+
import { conditionalItems, shownLayout, whenOf } from "./shown-layout.js";
|
|
3
3
|
//#region src/app/record-mode/use-shown-layout.ts
|
|
4
4
|
/**
|
|
5
5
|
* The layout items to draw: those meant for the form or the read view as the page is, and those with a record
|
|
6
6
|
* condition while it holds for the saved record. Conditional items stay out while their values load and on a preview.
|
|
7
7
|
*/
|
|
8
8
|
const useShownLayout = (items, scope, editing, preview) => {
|
|
9
|
-
const conditions = preview ? [] : conditionalItems(items).map((item) => item
|
|
9
|
+
const conditions = preview ? [] : conditionalItems(items).map((item) => whenOf(item));
|
|
10
10
|
const held = useConditions(conditions, scope);
|
|
11
11
|
return shownLayout(items, held, editing, preview);
|
|
12
12
|
};
|
package/dist/app/record-page.js
CHANGED
|
@@ -22,6 +22,7 @@ import { filledFields } from "./filled-fields.js";
|
|
|
22
22
|
import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
23
23
|
import { AppLink, useAppAddress } from "./use-app-address.js";
|
|
24
24
|
import { GroupSection } from "../record-view/group-section.js";
|
|
25
|
+
import { RecordTabs } from "../record-view/record-tabs.js";
|
|
25
26
|
import { SpecTable } from "../record-view/spec-table.js";
|
|
26
27
|
import { KeyFacts } from "../record-view/key-facts.js";
|
|
27
28
|
import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
|
|
@@ -47,6 +48,7 @@ import { CustomSection } from "./custom-section.js";
|
|
|
47
48
|
import { MapSection } from "./map/map-section.js";
|
|
48
49
|
import { RecordActions } from "./pages/record-actions.js";
|
|
49
50
|
import { groupOpen, toggledOpen } from "./group-open.js";
|
|
51
|
+
import { attentionTab, shownTab, splitTabs } from "./record-tabs.js";
|
|
50
52
|
import { RecordLines, findLinesResource } from "./record-lines.js";
|
|
51
53
|
import { draftStorageKey } from "./draft-storage.js";
|
|
52
54
|
import { useLines } from "./use-lines.js";
|
|
@@ -60,7 +62,7 @@ import { useRelatedRecords } from "./use-related-records.js";
|
|
|
60
62
|
import { IncomingCard } from "./incoming-card.js";
|
|
61
63
|
import { RelatedSection } from "./related-section.js";
|
|
62
64
|
import { relatedValue } from "./related-value.js";
|
|
63
|
-
import { Fragment, useMemo } from "react";
|
|
65
|
+
import { Fragment, useEffect, useMemo } from "react";
|
|
64
66
|
import { Check } from "lucide-react";
|
|
65
67
|
import { isApiError } from "@protobase/client";
|
|
66
68
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -155,15 +157,17 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
155
157
|
usePauseFocusRefresh(editor.dirty);
|
|
156
158
|
const openGroups = address?.kind === "record" ? address.open : void 0;
|
|
157
159
|
const atEdit = address?.kind === "record" && address.edit === true;
|
|
158
|
-
const
|
|
160
|
+
const addressedTab = address?.kind === "record" ? address.tab : void 0;
|
|
161
|
+
const recordAt = (open = [], edit = false, tab) => ({
|
|
159
162
|
kind: "record",
|
|
160
163
|
resource: model.name,
|
|
161
164
|
key: recordKey,
|
|
162
165
|
...edit && { edit },
|
|
163
|
-
...open.length > 0 && { open }
|
|
166
|
+
...open.length > 0 && { open },
|
|
167
|
+
...tab && { tab }
|
|
164
168
|
});
|
|
165
|
-
const readAddress = recordAt(openGroups);
|
|
166
|
-
const editAddress = recordAt(openGroups, true);
|
|
169
|
+
const readAddress = recordAt(openGroups, false, addressedTab);
|
|
170
|
+
const editAddress = recordAt(openGroups, true, addressedTab);
|
|
167
171
|
const leavesForm = shown === "form" && view?.recordMode !== void 0 && view.recordMode !== "edit";
|
|
168
172
|
const canDelete = can(permissions, "delete") && stored.permissions?.delete !== false;
|
|
169
173
|
const remove = useDeleteFlow({
|
|
@@ -204,6 +208,14 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
204
208
|
permissions: stored.permissions
|
|
205
209
|
};
|
|
206
210
|
const layout = useShownLayout(layoutFor(model, view, shown === "read"), scope, editing, preview);
|
|
211
|
+
const { before, tabs, after } = splitTabs(layout);
|
|
212
|
+
const tab = shownTab(tabs, addressedTab);
|
|
213
|
+
const needsAttention = (name) => editing && (editor.errors[name] !== void 0 || editor.conflicts[name] !== void 0);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (editor.rejections === 0) return;
|
|
216
|
+
const target = attentionTab(tabs, tab, needsAttention);
|
|
217
|
+
if (target) go(recordAt(openGroups, atEdit, target === tabs[0] ? void 0 : target.title), { replace: true });
|
|
218
|
+
}, [editor.rejections]);
|
|
207
219
|
const sidebarFields = layout.flatMap((item) => item.kind === "sidebar" ? item.fields : []);
|
|
208
220
|
const hasStatus = model.fields.status?.type === "enum";
|
|
209
221
|
const statusValue = stored.record.status;
|
|
@@ -353,8 +365,8 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
353
365
|
if (item.kind === "group") {
|
|
354
366
|
const children = item.items.flatMap(mainItem);
|
|
355
367
|
if (children.length === 0) return [];
|
|
356
|
-
const open = groupOpen(item, openGroups,
|
|
357
|
-
const toggle = item.collapsed ? recordAt(toggledOpen(openGroups, item.title), atEdit) : void 0;
|
|
368
|
+
const open = groupOpen(item, openGroups, needsAttention);
|
|
369
|
+
const toggle = item.collapsed ? recordAt(toggledOpen(openGroups, item.title), atEdit, addressedTab) : void 0;
|
|
358
370
|
return [/* @__PURE__ */ jsx(GroupSection, {
|
|
359
371
|
title: item.title,
|
|
360
372
|
open,
|
|
@@ -483,13 +495,26 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
483
495
|
className: "grid grid-cols-1 gap-8 md:grid-cols-[minmax(0,1fr)_300px] md:gap-12",
|
|
484
496
|
children: [/* @__PURE__ */ jsxs("main", {
|
|
485
497
|
className: "divide-y divide-border",
|
|
486
|
-
children: [
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
498
|
+
children: [
|
|
499
|
+
before.flatMap(mainItem),
|
|
500
|
+
tab && /* @__PURE__ */ jsx(RecordTabs, { tabs: tabs.map((each, index) => ({
|
|
501
|
+
title: each.title,
|
|
502
|
+
to: recordAt(openGroups, atEdit, index === 0 ? void 0 : each.title),
|
|
503
|
+
current: each === tab
|
|
504
|
+
})) }),
|
|
505
|
+
tab && /* @__PURE__ */ jsx("div", {
|
|
506
|
+
className: "divide-y divide-border",
|
|
507
|
+
children: tab.items.flatMap(mainItem)
|
|
508
|
+
}, `tab:${tab.title}`),
|
|
509
|
+
after.flatMap(mainItem),
|
|
510
|
+
linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
|
|
511
|
+
lines: linesModel,
|
|
512
|
+
draft: linesDraft,
|
|
513
|
+
vatRate: Number(stored.record.vatRate ?? 21) / 100,
|
|
514
|
+
saving: editor.saving,
|
|
515
|
+
locked: shown !== "form"
|
|
516
|
+
})
|
|
517
|
+
]
|
|
493
518
|
}), /* @__PURE__ */ jsxs("aside", {
|
|
494
519
|
className: "space-y-4 md:pt-8",
|
|
495
520
|
children: [
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { layoutItems } from "@protobase/schema/internal";
|
|
2
|
+
//#region src/app/record-tabs.ts
|
|
3
|
+
/** The shown layout around its tabs: the items before them, the tabs, and the items after. A view's tabs stand together. */
|
|
4
|
+
const splitTabs = (layout) => {
|
|
5
|
+
const first = layout.findIndex((item) => item.kind === "tab");
|
|
6
|
+
if (first === -1) return {
|
|
7
|
+
before: layout,
|
|
8
|
+
tabs: [],
|
|
9
|
+
after: []
|
|
10
|
+
};
|
|
11
|
+
const tabs = [];
|
|
12
|
+
for (const item of layout.slice(first)) {
|
|
13
|
+
if (item.kind !== "tab") break;
|
|
14
|
+
tabs.push(item);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
before: layout.slice(0, first),
|
|
18
|
+
tabs,
|
|
19
|
+
after: layout.slice(first + tabs.length)
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** The tab shown: the one the address names, else the first. */
|
|
23
|
+
const shownTab = (tabs, named) => tabs.find((tab) => tab.title === named) ?? tabs[0];
|
|
24
|
+
const tabFields = (tab) => layoutItems(tab.items).flatMap((item) => item.kind === "section" ? item.fields : []);
|
|
25
|
+
/** The tab to show after a turned-down save: the first that holds a field needing attention, unless the shown one does. */
|
|
26
|
+
const attentionTab = (tabs, shown, needsAttention) => {
|
|
27
|
+
if (!shown || tabFields(shown).some(needsAttention)) return void 0;
|
|
28
|
+
return tabs.find((tab) => tabFields(tab).some(needsAttention));
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { attentionTab, shownTab, splitTabs };
|
|
@@ -2,14 +2,21 @@ import { useEffect, useRef } from "react";
|
|
|
2
2
|
//#region src/app/use-focus-first-invalid.ts
|
|
3
3
|
/**
|
|
4
4
|
* Puts focus on the first control marked invalid inside the element the ref is on, each time `rejections` counts
|
|
5
|
-
* another save the server turned down, once the form shows why.
|
|
5
|
+
* another save the server turned down, once the form shows why, such as after the page moves to the tab that holds it.
|
|
6
6
|
*/
|
|
7
7
|
const useFocusFirstInvalid = (rejections) => {
|
|
8
8
|
const ref = useRef(null);
|
|
9
|
+
const pending = useRef(false);
|
|
9
10
|
useEffect(() => {
|
|
10
|
-
if (rejections
|
|
11
|
-
ref.current?.querySelector("[aria-invalid=\"true\"]")?.focus();
|
|
11
|
+
if (rejections > 0) pending.current = true;
|
|
12
12
|
}, [rejections]);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!pending.current) return;
|
|
15
|
+
const invalid = ref.current?.querySelector("[aria-invalid=\"true\"]");
|
|
16
|
+
if (!invalid) return;
|
|
17
|
+
invalid.focus();
|
|
18
|
+
pending.current = false;
|
|
19
|
+
});
|
|
13
20
|
return ref;
|
|
14
21
|
};
|
|
15
22
|
//#endregion
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { segmentedClass } from "../primitives/segmented.js";
|
|
3
|
+
import { AppLink } from "../app/use-app-address.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/record-view/record-tabs.tsx
|
|
6
|
+
/** The record page's tabs, links whose addresses show each; the shown one is the current page. */
|
|
7
|
+
const RecordTabs = ({ tabs }) => /* @__PURE__ */ jsx("nav", {
|
|
8
|
+
"aria-label": "Tabs",
|
|
9
|
+
className: "overflow-x-auto py-6",
|
|
10
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
11
|
+
className: segmentedClass.group("md"),
|
|
12
|
+
children: tabs.map((tab) => /* @__PURE__ */ jsx(AppLink, {
|
|
13
|
+
to: tab.to,
|
|
14
|
+
replace: true,
|
|
15
|
+
"aria-current": tab.current ? "page" : void 0,
|
|
16
|
+
className: cn(segmentedClass.option(tab.current, "md"), "whitespace-nowrap"),
|
|
17
|
+
children: tab.title
|
|
18
|
+
}, tab.title))
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
export { RecordTabs };
|