@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.
Files changed (30) hide show
  1. package/dist/app/current-address.d.ts +1 -0
  2. package/dist/app/current-address.js +2 -1
  3. package/dist/app/displays/board-column.js +63 -21
  4. package/dist/app/displays/board-columns.js +3 -1
  5. package/dist/app/displays/board-display.js +62 -11
  6. package/dist/app/displays/board-dnd.js +41 -5
  7. package/dist/app/displays/board-place.js +45 -0
  8. package/dist/app/displays/use-board-move.js +18 -9
  9. package/dist/app/list-page.js +3 -2
  10. package/dist/app/record-mode/shown-layout.js +16 -10
  11. package/dist/app/record-mode/use-shown-layout.js +2 -2
  12. package/dist/app/record-page.js +39 -14
  13. package/dist/app/record-tabs.js +31 -0
  14. package/dist/app/related-section.js +1 -1
  15. package/dist/app/use-focus-first-invalid.js +10 -3
  16. package/dist/package.js +1 -1
  17. package/dist/record-view/record-tabs.js +22 -0
  18. package/dist/shell/assets/app-CrmCFhlg.js +69 -0
  19. package/dist/shell/assets/index-BflGBHJH.css +2 -0
  20. package/dist/shell/assets/{index-BU2imrOR.js → index-Wkeo9_-S.js} +1 -1
  21. package/dist/shell/assets/{maplibre-CpkT3k-d.js → maplibre-BPlEFxat.js} +1 -1
  22. package/dist/shell/assets/{markdown-content-Cu8Q0N0V.js → markdown-content-DD9stPkw.js} +1 -1
  23. package/dist/shell/assets/{record-map-DMFFj00J.js → record-map-BGWjTC-n.js} +1 -1
  24. package/dist/shell/assets/{records-map-C1VrhxtO.js → records-map-Rt00G5XI.js} +1 -1
  25. package/dist/shell/assets/{shared-protobase-ui-DyjCtT2d.js → shared-protobase-ui-BdPdyvgu.js} +1 -1
  26. package/dist/shell/index.html +4 -4
  27. package/dist/shell/protobase.shell.json +1 -1
  28. package/package.json +4 -4
  29. package/dist/shell/assets/app-DivHWgft.js +0 -69
  30. package/dist/shell/assets/index-BtU8c1DC.css +0 -2
@@ -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 recordAt = (open = [], edit = false) => ({
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, (name) => editing && (editor.errors[name] !== void 0 || editor.conflicts[name] !== void 0));
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: [layout.flatMap(mainItem), linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
487
- lines: linesModel,
488
- draft: linesDraft,
489
- vatRate: Number(stored.record.vatRate ?? 21) / 100,
490
- saving: editor.saving,
491
- locked: shown !== "form"
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 === 0) return;
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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.22.8";
2
+ var version = "0.22.10";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -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 };