@protobase/ui 0.22.8 → 0.22.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/current-address.d.ts +1 -0
- package/dist/app/current-address.js +2 -1
- package/dist/app/displays/board-column.js +63 -21
- package/dist/app/displays/board-columns.js +3 -1
- package/dist/app/displays/board-display.js +62 -11
- package/dist/app/displays/board-dnd.js +41 -5
- package/dist/app/displays/board-place.js +45 -0
- package/dist/app/displays/use-board-move.js +18 -9
- package/dist/app/list-page.js +3 -2
- 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/related-section.js +1 -1
- 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-CrmCFhlg.js +69 -0
- package/dist/shell/assets/index-BflGBHJH.css +2 -0
- package/dist/shell/assets/{index-BU2imrOR.js → index-Wkeo9_-S.js} +1 -1
- package/dist/shell/assets/{maplibre-CpkT3k-d.js → maplibre-BPlEFxat.js} +1 -1
- package/dist/shell/assets/{markdown-content-Cu8Q0N0V.js → markdown-content-DD9stPkw.js} +1 -1
- package/dist/shell/assets/{record-map-DMFFj00J.js → record-map-BGWjTC-n.js} +1 -1
- package/dist/shell/assets/{records-map-C1VrhxtO.js → records-map-Rt00G5XI.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DyjCtT2d.js → shared-protobase-ui-BdPdyvgu.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-DivHWgft.js +0 -69
- package/dist/shell/assets/index-BtU8c1DC.css +0 -2
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 };
|
|
@@ -7,10 +7,10 @@ import { useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
|
7
7
|
import { ErrorBanner } from "./error-banner.js";
|
|
8
8
|
import { countQuery } from "./pages/use-layout-data.js";
|
|
9
9
|
import { searchAll } from "../live/search-all.js";
|
|
10
|
+
import { relatedHandOrder } from "./hand-order/hand-order.js";
|
|
10
11
|
import { relatedOpens } from "./related-opens.js";
|
|
11
12
|
import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
|
|
12
13
|
import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
|
|
13
|
-
import { relatedHandOrder } from "./hand-order/hand-order.js";
|
|
14
14
|
import { HandSortedList } from "./hand-order/hand-sorted-list.js";
|
|
15
15
|
import { SortButton } from "./hand-order/sort-button.js";
|
|
16
16
|
import { useState } from "react";
|
|
@@ -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 };
|