@protobase/ui 0.63.3 → 0.63.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.
Files changed (113) hide show
  1. package/dist/account/devices-section.js +1 -1
  2. package/dist/account/passkeys-section.js +1 -1
  3. package/dist/account/staff-sign-in-log.js +1 -1
  4. package/dist/activity-chart/activity-chart.js +2 -2
  5. package/dist/activity-chart/activity-tooltip.js +1 -1
  6. package/dist/app/app.js +1 -1
  7. package/dist/app/change-confirm/use-change-confirm.js +15 -18
  8. package/dist/app/charts/chart-view.js +1 -1
  9. package/dist/app/comments/comment-composer.js +1 -1
  10. package/dist/app/comments/comments-section.js +2 -2
  11. package/dist/app/create/create-record-form.js +1 -1
  12. package/dist/app/custom-section.js +1 -1
  13. package/dist/app/delete/use-delete-flow.js +1 -1
  14. package/dist/app/displays/board-column.js +1 -1
  15. package/dist/app/displays/calendar-display.js +1 -1
  16. package/dist/app/displays/cards-display.js +1 -1
  17. package/dist/app/displays/list-cards.js +1 -1
  18. package/dist/app/displays/timeline-display.js +1 -1
  19. package/dist/app/displays/use-board-move.js +1 -1
  20. package/dist/app/displays/use-calendar-move.js +1 -1
  21. package/dist/app/embedded-list/embedded-list.js +1 -1
  22. package/dist/app/field-editor.js +2 -2
  23. package/dist/app/gallery/gallery-section.js +1 -1
  24. package/dist/app/gallery/photo-details.js +1 -1
  25. package/dist/app/hand-order/hand-sorted-list.js +1 -1
  26. package/dist/app/hand-order/use-hand-order.js +1 -1
  27. package/dist/app/history/history-section.js +1 -1
  28. package/dist/app/incoming-card.js +1 -1
  29. package/dist/app/lines-plan.js +116 -0
  30. package/dist/app/list-edit/use-list-edits.js +1 -1
  31. package/dist/app/list-order.js +7 -1
  32. package/dist/app/new-version/new-version-toast.js +1 -1
  33. package/dist/app/pages/blocks/field.js +1 -1
  34. package/dist/app/pages/blocks/modal-form.js +2 -2
  35. package/dist/app/pages/blocks/settings.js +2 -2
  36. package/dist/app/pages/record-actions.js +1 -1
  37. package/dist/app/pages/use-run-action.js +1 -1
  38. package/dist/app/picker/picker-combobox.js +1 -1
  39. package/dist/app/record-chart/record-chart-section.js +2 -2
  40. package/dist/app/record-lines-cell.js +47 -0
  41. package/dist/app/record-lines.js +103 -26
  42. package/dist/app/record-messages.js +8 -2
  43. package/dist/app/record-page.js +18 -20
  44. package/dist/app/record-row/record-row-body.js +1 -1
  45. package/dist/app/record-row/record-row.js +1 -1
  46. package/dist/app/record-row/saved-record-row.js +1 -1
  47. package/dist/app/related-cards/related-card.js +1 -1
  48. package/dist/app/related-cards/related-cards.js +1 -1
  49. package/dist/app/related-section.js +1 -1
  50. package/dist/app/saved/saved-list.js +1 -1
  51. package/dist/app/searches/saved-search-rows.js +1 -1
  52. package/dist/app/searches/saved-search-section.js +1 -1
  53. package/dist/app/searches/saved-searches.js +1 -1
  54. package/dist/app/searches/search-controls.js +1 -1
  55. package/dist/app/searches/update-search-dialog.js +1 -1
  56. package/dist/app/searches/use-save-search.js +1 -1
  57. package/dist/app/sensitive-field.js +1 -1
  58. package/dist/app/use-lines-draft.js +94 -35
  59. package/dist/app/use-lines.js +21 -7
  60. package/dist/app/use-record-editor.js +15 -10
  61. package/dist/app/use-record-lines.js +47 -0
  62. package/dist/app-shell/profile-menu.js +2 -2
  63. package/dist/auth/forgot-password-form.js +1 -1
  64. package/dist/auth/sign-in-form.js +1 -1
  65. package/dist/calls/call-guests-panel.js +1 -1
  66. package/dist/calls/call-rings.js +1 -1
  67. package/dist/chat/chat-message.js +1 -1
  68. package/dist/data/use-list.d.ts +27 -1
  69. package/dist/data/use-list.js +10 -3
  70. package/dist/data-grid/cell-text.js +3 -3
  71. package/dist/data-grid/cells.js +3 -3
  72. package/dist/data-grid/grid-row.js +1 -1
  73. package/dist/forms/public-form-page.js +1 -1
  74. package/dist/index.js +3 -3
  75. package/dist/inline-child-table/columns.js +7 -2
  76. package/dist/inline-child-table/inline-child-table.js +29 -48
  77. package/dist/inline-child-table/line-row.js +16 -44
  78. package/dist/inline-child-table/messages.js +4 -18
  79. package/dist/notifications/bell/notification-item.js +1 -1
  80. package/dist/organizations/invitation-page.js +1 -1
  81. package/dist/package.js +1 -1
  82. package/dist/primitives/dialog.d.ts +7 -2
  83. package/dist/primitives/dialog.js +9 -4
  84. package/dist/record-view/field.js +1 -1
  85. package/dist/record-view/messages.js +0 -4
  86. package/dist/record-view/record-header.js +2 -2
  87. package/dist/shell/assets/app-BPv_SjnU.js +9 -0
  88. package/dist/shell/assets/{calls-CEW8hdPi.js → calls-CPiQcC70.js} +2 -2
  89. package/dist/shell/assets/index-Or2wt-ck.css +2 -0
  90. package/dist/shell/assets/{index-D7O0sMwH.js → index-_ZpZ2ro0.js} +2 -2
  91. package/dist/shell/assets/{records-map-BIDl1ull.js → records-map-DU29d6QK.js} +2 -2
  92. package/dist/shell/assets/shared-protobase-ui-Cab5nqST.js +2 -0
  93. package/dist/shell/assets/{vendor-xNCj9Gc_.js → vendor-iwLB0x-s.js} +39 -39
  94. package/dist/shell/index.html +5 -5
  95. package/dist/shell/protobase.shell.json +2 -2
  96. package/dist/shell/protobase.sw.js +2 -2
  97. package/dist/shell-source-maps/assets/app-BPv_SjnU.js.map +1 -0
  98. package/dist/shell-source-maps/assets/{calls-CEW8hdPi.js.map → calls-CPiQcC70.js.map} +1 -1
  99. package/dist/shell-source-maps/assets/{index-D7O0sMwH.js.map → index-_ZpZ2ro0.js.map} +1 -1
  100. package/dist/shell-source-maps/assets/{records-map-BIDl1ull.js.map → records-map-DU29d6QK.js.map} +1 -1
  101. package/dist/shell-source-maps/assets/{shared-protobase-ui-F5elmh4N.js.map → shared-protobase-ui-Cab5nqST.js.map} +1 -1
  102. package/dist/shell-source-maps/assets/vendor-iwLB0x-s.js.map +1 -0
  103. package/dist/status/status-dialog.js +1 -1
  104. package/package.json +4 -4
  105. package/dist/inline-child-table/number-cell.js +0 -25
  106. package/dist/inline-child-table/totals-footer.js +0 -39
  107. package/dist/mocks/invoices.js +0 -13
  108. package/dist/mocks/products.js +0 -167
  109. package/dist/shell/assets/app-Bm6h0l5D.js +0 -9
  110. package/dist/shell/assets/index-Be8nCyRW.css +0 -2
  111. package/dist/shell/assets/shared-protobase-ui-F5elmh4N.js +0 -2
  112. package/dist/shell-source-maps/assets/app-Bm6h0l5D.js.map +0 -1
  113. package/dist/shell-source-maps/assets/vendor-xNCj9Gc_.js.map +0 -1
@@ -0,0 +1,47 @@
1
+ import { FieldControlContext } from "../primitives/field-control.js";
2
+ import { FieldEditor } from "./field-editor.js";
3
+ import { fieldMessage } from "./field-errors.js";
4
+ import { text } from "./record-messages.js";
5
+ import { useId } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/app/record-lines-cell.tsx
8
+ /** One field of a line, edited with the record page's own editor; its label is for screen readers, as the column heading shows it. */
9
+ const LineCell = ({ field, hints, label, lineNumber, resource, value, stored, relationLabel, relationTo, relationPicker, locked, problem, onChange }) => {
10
+ const id = useId();
11
+ const message = problem && fieldMessage(field, label, problem);
12
+ return /* @__PURE__ */ jsxs(FieldControlContext.Provider, {
13
+ value: {
14
+ id,
15
+ invalid: Boolean(problem),
16
+ required: false,
17
+ ...message && { describedBy: `${id}-error` }
18
+ },
19
+ children: [
20
+ /* @__PURE__ */ jsx("label", {
21
+ htmlFor: id,
22
+ className: "sr-only",
23
+ children: text("lineField", label, lineNumber)
24
+ }),
25
+ /* @__PURE__ */ jsx(FieldEditor, {
26
+ field,
27
+ hints,
28
+ value,
29
+ stored,
30
+ relationLabel,
31
+ relationTo,
32
+ relationPicker,
33
+ locked,
34
+ invalid: Boolean(problem),
35
+ resource,
36
+ onChange
37
+ }),
38
+ message && /* @__PURE__ */ jsx("p", {
39
+ id: `${id}-error`,
40
+ className: "px-1 pt-0.5 text-xs text-danger-text",
41
+ children: message
42
+ })
43
+ ]
44
+ });
45
+ };
46
+ //#endregion
47
+ export { LineCell };
@@ -1,40 +1,117 @@
1
- import { usePermissions } from "./meta-gate.js";
2
- import { InlineChildTable } from "../inline-child-table/inline-child-table.js";
1
+ import { useAdminMeta, usePermissions } from "./meta-gate.js";
3
2
  import { Section } from "../record-view/section.js";
4
- import { can } from "./permissions.js";
3
+ import { formatInt } from "../format/number.js";
4
+ import { humanize } from "../live/naming.js";
5
+ import { recordTitle } from "../live/model-helpers.js";
6
+ import { isChosen, keyValue } from "./create/create-values.js";
7
+ import { can, rowAllows } from "./permissions.js";
5
8
  import { text } from "./record-messages.js";
6
- import { jsx } from "react/jsx-runtime";
9
+ import { lineTableMinWidth } from "../inline-child-table/columns.js";
10
+ import { InlineChildTable } from "../inline-child-table/inline-child-table.js";
11
+ import { LineCell } from "./record-lines-cell.js";
12
+ import "./use-lines.js";
13
+ import { useState } from "react";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
7
15
  //#region src/app/record-lines.tsx
8
- /** A resource that holds numbered lines of this one: it points at the parent and has `position` and `quantity`. */
9
- const findLinesResource = (resources, parent) => Object.values(resources).find((candidate) => candidate.name !== parent.name && candidate.fields.position && candidate.fields.quantity && Object.values(candidate.fields).some((field) => field.type === "relation" && field.relation?.resource === parent.name));
10
- const toLine = (record) => ({
11
- id: String(record.id),
12
- productId: String(record.productId ?? ""),
13
- description: String(record.description ?? ""),
14
- quantity: Number(record.quantity),
15
- unitPriceCents: Math.round(Number(record.unitPrice) * 100)
16
- });
17
- /** The lines of a record. Reordering only changes `draft`; the record's Save writes it, in one batch. */
18
- const RecordLines = ({ lines, draft, vatRate, saving, locked = false }) => {
19
- const canReorder = can(usePermissions(lines.name), "update");
20
- return /* @__PURE__ */ jsx(Section, {
16
+ /**
17
+ * The lines of a record as a table of their view's columns, edited in place: a line's fields, new lines, removed ones
18
+ * and their order only change the draft; the record's Save writes it all, in one batch. A line the server turns down
19
+ * says why on its row.
20
+ */
21
+ const RecordLines = ({ lines, editable, saving }) => {
22
+ const { resources, views } = useAdminMeta();
23
+ const { model, columns, data, draft } = lines;
24
+ const permissions = usePermissions(model.name);
25
+ const view = views[model.name];
26
+ const [picked, setPicked] = useState({});
27
+ const open = editable && data.complete && !data.loading && !saving;
28
+ const canUpdate = open && can(permissions, "update");
29
+ const canCreate = open && can(permissions, "create");
30
+ const canDelete = open && can(permissions, "delete");
31
+ const writable = (line) => line.stored ? canUpdate && rowAllows(line.stored, "update") : canCreate;
32
+ const placeOf = (line) => draft.shown.indexOf(line) + 1;
33
+ const rejected = (line) => draft.rejection?.id === line.id ? draft.rejection : void 0;
34
+ const tableColumns = columns.map((name) => {
35
+ const field = model.fields[name];
36
+ const hints = view?.fields[name];
37
+ const label = hints?.label ?? humanize(name);
38
+ const target = field.relation ? resources[field.relation.resource] : void 0;
39
+ const pickable = target && target.primaryKey.length === 1 ? target : void 0;
40
+ return {
41
+ key: name,
42
+ label,
43
+ ...[
44
+ "integer",
45
+ "bigint",
46
+ "decimal"
47
+ ].includes(field.type) && {
48
+ align: "right",
49
+ width: "minmax(6rem,0.6fr)"
50
+ },
51
+ cell: (line) => {
52
+ const value = draft.value(line.id, name);
53
+ const chosen = value === null || value === void 0 || value === "" ? void 0 : String(value);
54
+ return /* @__PURE__ */ jsx(LineCell, {
55
+ field,
56
+ hints,
57
+ label,
58
+ lineNumber: placeOf(line),
59
+ resource: model.name,
60
+ value,
61
+ stored: line.stored?.[name],
62
+ relationLabel: chosen === void 0 ? void 0 : picked[`${line.id}/${name}`] ?? data.labels[name]?.[chosen],
63
+ relationTo: pickable && chosen !== void 0 ? {
64
+ kind: "record",
65
+ resource: pickable.name,
66
+ key: chosen
67
+ } : void 0,
68
+ relationPicker: pickable && {
69
+ target: pickable,
70
+ view: views[pickable.name]
71
+ },
72
+ locked: !writable(line),
73
+ problem: rejected(line)?.fields[name],
74
+ onChange: (next) => {
75
+ if (!pickable || !isChosen(next)) return draft.change(line.id, name, next);
76
+ setPicked((current) => ({
77
+ ...current,
78
+ [`${line.id}/${name}`]: next.label
79
+ }));
80
+ draft.change(line.id, name, keyValue(pickable, next.key));
81
+ }
82
+ });
83
+ }
84
+ };
85
+ });
86
+ const lineError = (line) => {
87
+ const rejection = rejected(line);
88
+ if (!rejection || columns.some((name) => rejection.fields[name])) return void 0;
89
+ return rejection.message;
90
+ };
91
+ return /* @__PURE__ */ jsxs(Section, {
21
92
  title: text("lines"),
22
93
  help: text("linesHelp"),
23
- children: /* @__PURE__ */ jsx("div", {
94
+ children: [!data.complete && /* @__PURE__ */ jsx("p", {
95
+ className: "mb-2 text-muted-foreground",
96
+ children: text("tooManyLines", formatInt(1e3))
97
+ }), /* @__PURE__ */ jsx("div", {
24
98
  className: "-mx-4 overflow-x-auto px-4 [-webkit-overflow-scrolling:touch] md:mx-0 md:px-0",
25
99
  children: /* @__PURE__ */ jsx("div", {
26
- className: "min-w-[560px]",
100
+ style: { minWidth: lineTableMinWidth(tableColumns) },
27
101
  children: /* @__PURE__ */ jsx(InlineChildTable, {
28
- lines: draft.shown.map(toLine),
29
- onChange: () => void 0,
102
+ lines: draft.shown,
103
+ columns: tableColumns,
104
+ lineName: (line) => recordTitle(model, view, line.stored ?? {}, "") || text("lineNumber", placeOf(line)),
30
105
  onReorder: (next) => draft.setOrder(next.map((line) => line.id)),
31
- vatRate,
32
- readOnly: true,
33
- reorderable: canReorder && !saving && !locked
106
+ reorderable: canUpdate,
107
+ onAdd: canCreate ? () => void draft.add() : void 0,
108
+ onRemove: canCreate || canDelete ? (line) => draft.remove(line.id) : void 0,
109
+ removable: (line) => line.stored ? canDelete && rowAllows(line.stored, "delete") : canCreate,
110
+ lineError
34
111
  })
35
112
  })
36
- })
113
+ })]
37
114
  });
38
115
  };
39
116
  //#endregion
40
- export { RecordLines, findLinesResource };
117
+ export { RecordLines };
@@ -36,7 +36,10 @@ const text = defineMessages({
36
36
  couldNotLoadRelated: "Could not load",
37
37
  relatedRecords: "Related records",
38
38
  lines: "Lines",
39
- linesHelp: "What the document bills for. Drag the handle, or use the menu, to change the order; Save applies it."
39
+ linesHelp: "What the document bills for. Edit a line in place, add one, or drag the handle to change the order; Save applies it all.",
40
+ lineNumber: (place) => `Line ${place}`,
41
+ lineField: (label, place) => `${label}, line ${place}`,
42
+ tooManyLines: (limit) => `This record has more than ${limit} lines, so they show here read-only.`
40
43
  },
41
44
  nl: {
42
45
  details: "Details",
@@ -73,7 +76,10 @@ const text = defineMessages({
73
76
  couldNotLoadRelated: "Kon niet laden",
74
77
  relatedRecords: "Gerelateerde records",
75
78
  lines: "Regels",
76
- linesHelp: "Waar het document voor factureert. Sleep de greep, of gebruik het menu, om de volgorde te wijzigen; Bewaren past hem toe."
79
+ linesHelp: "Waar het document voor factureert. Wijzig een regel ter plekke, voeg er een toe, of sleep de greep om de volgorde te wijzigen; Bewaren past alles toe.",
80
+ lineNumber: (place) => `Regel ${place}`,
81
+ lineField: (label, place) => `${label}, regel ${place}`,
82
+ tooManyLines: (limit) => `Dit record heeft meer dan ${limit} regels, dus ze staan hier alleen ter inzage.`
77
83
  }
78
84
  });
79
85
  //#endregion
@@ -5,12 +5,8 @@ import { useClient } from "../data/api-provider.js";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
7
7
  import { Button, buttonClass } from "../primitives/button.js";
8
- import { Skeleton } from "../primitives/skeleton.js";
9
- import { inApp } from "../app-shell/breadcrumb.js";
10
- import { Field } from "../record-view/field.js";
11
- import { Section } from "../record-view/section.js";
12
- import { MetadataCard } from "../record-view/record-sidebar.js";
13
8
  import { RecordHeader } from "../record-view/record-header.js";
9
+ import { Section } from "../record-view/section.js";
14
10
  import { humanize } from "../live/naming.js";
15
11
  import { markdownExcerpt } from "../markdown/markdown-excerpt.js";
16
12
  import { isMarkdown } from "../markdown/markdown-hint.js";
@@ -18,13 +14,17 @@ import { contactFormat } from "../live/contact-links.js";
18
14
  import { toneFor } from "../live/columns-from-model.js";
19
15
  import { displayValue, valueTitle } from "../live/display-value.js";
20
16
  import { filledFields } from "./filled-fields.js";
17
+ import { Skeleton } from "../primitives/skeleton.js";
18
+ import { inApp } from "../app-shell/breadcrumb.js";
21
19
  import { text } from "./messages.js";
22
20
  import { useNavigationGuard } from "./router.js";
23
21
  import { AppLink, useAppAddress } from "./use-app-address.js";
24
22
  import { GroupSection } from "../record-view/group-section.js";
25
23
  import { RecordTabs } from "../record-view/record-tabs.js";
24
+ import { Field } from "../record-view/field.js";
26
25
  import { SpecTable } from "../record-view/spec-table.js";
27
26
  import { KeyFacts } from "../record-view/key-facts.js";
27
+ import { MetadataCard } from "../record-view/record-sidebar.js";
28
28
  import { fromDraft, isRequiredOnEdit } from "../live/field-values.js";
29
29
  import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/record-mode.js";
30
30
  import { isChosen, keyValue } from "./create/create-values.js";
@@ -53,10 +53,9 @@ import { RecordChartSection } from "./record-chart/record-chart-section.js";
53
53
  import { RecordActions } from "./pages/record-actions.js";
54
54
  import { groupOpen, toggledOpen } from "./group-open.js";
55
55
  import { attentionTab, shownTab, splitTabs } from "./record-tabs.js";
56
- import { RecordLines, findLinesResource } from "./record-lines.js";
56
+ import { RecordLines } from "./record-lines.js";
57
57
  import { draftStorageKey } from "./draft-storage.js";
58
- import { useLines } from "./use-lines.js";
59
- import { useLinesDraft } from "./use-lines-draft.js";
58
+ import { useRecordLines } from "./use-record-lines.js";
60
59
  import { GallerySection } from "./gallery/gallery-section.js";
61
60
  import { HistorySection } from "./history/history-section.js";
62
61
  import { CommentsSection } from "./comments/comments-section.js";
@@ -67,7 +66,7 @@ import { useRelatedRecords } from "./use-related-records.js";
67
66
  import { IncomingCard } from "./incoming-card.js";
68
67
  import { RelatedSection } from "./related-section.js";
69
68
  import { relatedValue } from "./related-value.js";
70
- import { Fragment, useEffect, useMemo } from "react";
69
+ import { Fragment, useEffect } from "react";
71
70
  import { Check } from "lucide-react";
72
71
  import { isApiError } from "@protobase/client";
73
72
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -174,9 +173,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
174
173
  const view = meta.views[model.name];
175
174
  const title = useRecordTitle()(model, view, stored.record, recordKey);
176
175
  const { state: auth } = useAuth();
177
- const linesModel = useMemo(() => findLinesResource(meta.resources, model), [meta.resources, model]);
178
- const linesData = useLines(linesModel, model, recordKey);
179
- const linesDraft = useLinesDraft(linesModel, linesData.items);
176
+ const lines = useRecordLines(model, recordKey);
180
177
  const editor = useRecordEditor({
181
178
  model,
182
179
  stored,
@@ -184,7 +181,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
184
181
  mode: view?.saveFeedback ?? "toast",
185
182
  subject: title,
186
183
  draftKey: !preview && auth.kind === "signed-in" ? draftStorageKey(auth.user.id, model.name, recordKey) : void 0,
187
- lines: linesDraft
184
+ lines: lines.draft
188
185
  });
189
186
  const queryClient = useQueryClient();
190
187
  const client = useClient();
@@ -552,12 +549,13 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
552
549
  children: tab.items.flatMap(mainItem)
553
550
  }, `tab:${tab.title}`),
554
551
  after.flatMap(mainItem),
555
- linesModel && !preview && /* @__PURE__ */ jsx(RecordLines, {
556
- lines: linesModel,
557
- draft: linesDraft,
558
- vatRate: Number(stored.record.vatRate ?? 21) / 100,
559
- saving: editor.saving,
560
- locked: shown !== "form"
552
+ lines.model && !preview && /* @__PURE__ */ jsx(RecordLines, {
553
+ lines: {
554
+ ...lines,
555
+ model: lines.model
556
+ },
557
+ editable: shown === "form",
558
+ saving: editor.saving
561
559
  })
562
560
  ]
563
561
  }), /* @__PURE__ */ jsxs("aside", {
@@ -604,7 +602,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
604
602
  model,
605
603
  recordKey,
606
604
  layout,
607
- lines: linesModel
605
+ lines: lines.model
608
606
  })
609
607
  ]
610
608
  })]
@@ -1,10 +1,10 @@
1
1
  import { formatTag } from "../../locale/active-locale.js";
2
2
  import { useAdminMeta } from "../meta-gate.js";
3
3
  import { useIsXs } from "../../lib/use-media-query.js";
4
- import { Skeleton } from "../../primitives/skeleton.js";
5
4
  import { humanize } from "../../live/naming.js";
6
5
  import { displaysOf, loadedFields } from "../displays/list-displays.js";
7
6
  import { defaultOrderBy } from "../list-order.js";
7
+ import { Skeleton } from "../../primitives/skeleton.js";
8
8
  import { AppLink } from "../use-app-address.js";
9
9
  import { ErrorBanner } from "../error-banner.js";
10
10
  import { viewAllAddress } from "../embedded-list/embedded-display.js";
@@ -1,5 +1,5 @@
1
- import { Skeleton } from "../../primitives/skeleton.js";
2
1
  import { displayRow } from "../displays/list-displays.js";
2
+ import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { usePagedRows } from "../../data-grid/use-paged-rows.js";
4
4
  import { RecordCard } from "../displays/record-card.js";
5
5
  import { rowOffset } from "./row-window.js";
@@ -1,7 +1,7 @@
1
1
  import { useAdminMeta } from "../meta-gate.js";
2
- import { Skeleton } from "../../primitives/skeleton.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { listedResource } from "../current-address.js";
4
+ import { Skeleton } from "../../primitives/skeleton.js";
5
5
  import { ErrorBanner } from "../error-banner.js";
6
6
  import { savedScope } from "../saved/saved-records.js";
7
7
  import { useSavedRecords } from "../saved/use-saved-records.js";
@@ -1,6 +1,6 @@
1
1
  import { useAdminMeta } from "../meta-gate.js";
2
- import { Skeleton } from "../../primitives/skeleton.js";
3
2
  import { recordKey } from "../../live/model-helpers.js";
3
+ import { Skeleton } from "../../primitives/skeleton.js";
4
4
  import { AppLink } from "../use-app-address.js";
5
5
  import { useRecordTitle } from "../titles/record-titles.js";
6
6
  import { RecordScope } from "../pages/record-scope.js";
@@ -1,7 +1,7 @@
1
1
  import { useIsXs } from "../../lib/use-media-query.js";
2
- import { Skeleton } from "../../primitives/skeleton.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { defaultOrderBy } from "../list-order.js";
4
+ import { Skeleton } from "../../primitives/skeleton.js";
5
5
  import { AppLink } from "../use-app-address.js";
6
6
  import { ErrorBanner } from "../error-banner.js";
7
7
  import { scopeOf, useCount, useListPage } from "../pages/use-layout-data.js";
@@ -1,9 +1,9 @@
1
1
  import { keys } from "../data/query-keys.js";
2
2
  import { useClient } from "../data/api-provider.js";
3
3
  import { useAdminMeta, usePermissions } from "./meta-gate.js";
4
- import { Skeleton } from "../primitives/skeleton.js";
5
4
  import { Section } from "../record-view/section.js";
6
5
  import { listColumnNames } from "../live/columns-from-model.js";
6
+ import { Skeleton } from "../primitives/skeleton.js";
7
7
  import { ErrorBanner } from "./error-banner.js";
8
8
  import { countQuery } from "./pages/use-layout-data.js";
9
9
  import { RelatedCards } from "./related-cards/related-cards.js";
@@ -1,5 +1,5 @@
1
- import { Skeleton } from "../../primitives/skeleton.js";
2
1
  import { humanize } from "../../live/naming.js";
2
+ import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { ErrorBanner } from "../error-banner.js";
4
4
  import { PageHeader } from "../../app-shell/page-header.js";
5
5
  import { savedScope } from "./saved-records.js";
@@ -1,7 +1,7 @@
1
1
  import { useAdminMeta } from "../meta-gate.js";
2
- import { Skeleton } from "../../primitives/skeleton.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { recordKey } from "../../live/model-helpers.js";
4
+ import { Skeleton } from "../../primitives/skeleton.js";
5
5
  import { AppLink } from "../use-app-address.js";
6
6
  import { ErrorBanner } from "../error-banner.js";
7
7
  import { text } from "./messages.js";
@@ -1,8 +1,8 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
2
  import { Button } from "../../primitives/button.js";
3
- import { useToast } from "../../toasts/toast-provider.js";
4
3
  import { humanize } from "../../live/naming.js";
5
4
  import { AppLink } from "../use-app-address.js";
5
+ import { useToast } from "../../toasts/toast-provider.js";
6
6
  import { useRefreshResource } from "../pages/use-layout-data.js";
7
7
  import { text } from "./messages.js";
8
8
  import { InlineFilterChips } from "./filter-chip-list.js";
@@ -1,7 +1,7 @@
1
1
  import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
2
- import { Skeleton } from "../../primitives/skeleton.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { recordKey } from "../../live/model-helpers.js";
4
+ import { Skeleton } from "../../primitives/skeleton.js";
5
5
  import { AppLink } from "../use-app-address.js";
6
6
  import { ErrorBanner } from "../error-banner.js";
7
7
  import { slotted } from "../../locale/slotted.js";
@@ -1,7 +1,7 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
- import { useToast } from "../../toasts/toast-provider.js";
3
2
  import { humanize } from "../../live/naming.js";
4
3
  import { Switch } from "../../primitives/switch.js";
4
+ import { useToast } from "../../toasts/toast-provider.js";
5
5
  import { useRefreshResource } from "../pages/use-layout-data.js";
6
6
  import { text } from "./messages.js";
7
7
  import { useId } from "react";
@@ -1,9 +1,9 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
2
  import { Button } from "../../primitives/button.js";
3
3
  import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
4
- import { Skeleton } from "../../primitives/skeleton.js";
5
4
  import { humanize } from "../../live/naming.js";
6
5
  import { defaultOrderBy } from "../list-order.js";
6
+ import { Skeleton } from "../../primitives/skeleton.js";
7
7
  import { countQuery } from "../pages/use-layout-data.js";
8
8
  import { text } from "./messages.js";
9
9
  import { useFilterChips } from "./use-filter-chips.js";
@@ -1,9 +1,9 @@
1
1
  import { useClient } from "../../data/api-provider.js";
2
2
  import { useAdminMeta } from "../meta-gate.js";
3
- import { useToast } from "../../toasts/toast-provider.js";
4
3
  import { humanize } from "../../live/naming.js";
5
4
  import { recordKey } from "../../live/model-helpers.js";
6
5
  import { useAppAddress } from "../use-app-address.js";
6
+ import { useToast } from "../../toasts/toast-provider.js";
7
7
  import { can } from "../permissions.js";
8
8
  import { useRefreshResource } from "../pages/use-layout-data.js";
9
9
  import { text } from "./messages.js";
@@ -1,9 +1,9 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Button } from "../primitives/button.js";
3
- import { Input } from "../primitives/input.js";
4
3
  import { humanize } from "../live/naming.js";
5
4
  import { displayValue } from "../live/display-value.js";
6
5
  import { text } from "./messages.js";
6
+ import { Input } from "../primitives/input.js";
7
7
  import { toDraft } from "../live/field-values.js";
8
8
  import { ErrorBanner } from "./error-banner.js";
9
9
  import { ConfirmSignIn } from "./confirm-sign-in.js";
@@ -1,44 +1,103 @@
1
- import { recordKey } from "../live/model-helpers.js";
2
- import { useCallback, useMemo, useState } from "react";
3
- import { encodeKey } from "@protobase/schema/internal";
1
+ import { recordId } from "../live/model-helpers.js";
2
+ import { toFieldDraft } from "../live/field-values.js";
3
+ import { fieldErrors } from "./field-errors.js";
4
+ import { emptyLinesState, planLinesOps, shownIds } from "./lines-plan.js";
5
+ import { useMemo, useRef, useState } from "react";
4
6
  //#region src/app/use-lines-draft.ts
7
+ const without = (source, name) => Object.fromEntries(Object.entries(source).filter(([key]) => key !== name));
5
8
  /**
6
- * The order the person has given the lines of a record, kept in the form until Save writes it, together with the
7
- * record, in one batch. `shown` is the lines in that order; `op()` is the batch operation that makes it permanent.
9
+ * The changes the person made to a record's lines, kept in the form until Save writes them, together with the record,
10
+ * in one batch: the order, new lines, typed fields and removed lines. `shown` is the lines as they are on screen;
11
+ * `plan()` is the batch that makes them permanent.
8
12
  */
9
- const useLinesDraft = (model, stored) => {
10
- const [order, setOrder] = useState();
11
- const ids = stored.map((line) => String(line.id));
12
- const shown = useMemo(() => {
13
- if (!order) return stored;
14
- const known = order.flatMap((id) => stored.filter((line) => String(line.id) === id));
15
- const added = stored.filter((line) => !order.includes(String(line.id)));
16
- return [...known, ...added];
17
- }, [order, stored]);
18
- const shownIds = shown.map((line) => String(line.id));
19
- const dirty = order !== void 0 && shownIds.some((id, index) => id !== ids[index]);
20
- const op = useCallback(() => {
21
- if (!model || !dirty) return [];
22
- return [{
23
- op: "reorder",
24
- resource: model.name,
25
- field: "position",
26
- keys: shown.map((line) => recordKey(model, line)),
27
- etags: Object.fromEntries(shown.flatMap((line) => typeof line.etag === "string" ? [[encodeKey(recordKey(model, line)), line.etag]] : []))
28
- }];
29
- }, [
30
- model,
31
- dirty,
32
- shown
33
- ]);
13
+ const useLinesDraft = (target, stored) => {
14
+ const [state, setState] = useState(emptyLinesState);
15
+ const [rejection, setRejection] = useState();
16
+ const nextId = useRef(1);
17
+ const byId = useMemo(() => new Map(target ? stored.map((line) => [recordId(target.model, line), line]) : []), [target, stored]);
18
+ const shown = shownIds([...byId.keys()], state).map((id) => ({
19
+ id,
20
+ stored: byId.get(id)
21
+ }));
22
+ const plan = () => target ? planLinesOps({
23
+ ...target,
24
+ stored,
25
+ state
26
+ }) : {
27
+ ops: [],
28
+ rows: []
29
+ };
30
+ const dirty = plan().ops.length > 0;
31
+ const order = useMemo(() => {
32
+ const kept = shownIds([...byId.keys()], state).filter((id) => byId.has(id));
33
+ const before = [...byId.keys()].filter((id) => kept.includes(id));
34
+ return state.order !== void 0 && kept.some((id, index) => id !== before[index]) ? kept : void 0;
35
+ }, [byId, state]);
36
+ /** The value a line's field editor holds: what was typed, else the stored value as the editor takes it. */
37
+ const value = (id, name) => {
38
+ const typed = state.edits[id];
39
+ if (typed && name in typed) return typed[name];
40
+ const field = target?.model.fields[name];
41
+ const current = byId.get(id)?.[name];
42
+ if (!field) return current;
43
+ return ["boolean", "file"].includes(field.type) ? current : toFieldDraft(field, current);
44
+ };
45
+ const forget = (id) => setRejection((current) => current?.id === id ? void 0 : current);
34
46
  return {
35
47
  shown,
36
48
  dirty,
37
- /** The ids in the order on screen, for keeping a draft. */
38
- order: dirty ? shownIds : void 0,
39
- setOrder: (next) => setOrder(next),
40
- reset: () => setOrder(void 0),
41
- op
49
+ /** The stored lines' ids in the order on screen, once moved, for keeping a draft. */
50
+ order,
51
+ value,
52
+ change: (id, name, next) => {
53
+ forget(id);
54
+ setState((current) => ({
55
+ ...current,
56
+ edits: {
57
+ ...current.edits,
58
+ [id]: {
59
+ ...current.edits[id],
60
+ [name]: next
61
+ }
62
+ }
63
+ }));
64
+ },
65
+ /** Adds an empty line at the end; the database's defaults fill what is left empty when it is saved. */
66
+ add: () => {
67
+ const id = `new:${nextId.current++}`;
68
+ setState((current) => ({
69
+ ...current,
70
+ added: [...current.added, id]
71
+ }));
72
+ return id;
73
+ },
74
+ remove: (id) => {
75
+ forget(id);
76
+ setState((current) => current.added.includes(id) ? {
77
+ ...current,
78
+ added: current.added.filter((each) => each !== id),
79
+ edits: without(current.edits, id)
80
+ } : {
81
+ ...current,
82
+ removed: [...current.removed, id]
83
+ });
84
+ },
85
+ setOrder: (next) => setState((current) => ({
86
+ ...current,
87
+ order: next
88
+ })),
89
+ reset: () => {
90
+ setState(emptyLinesState);
91
+ setRejection(void 0);
92
+ },
93
+ /** The server turned down the write of line `id`: its fields and message show on that line until it changes. */
94
+ reject: (id, error) => setRejection({
95
+ id,
96
+ fields: fieldErrors(error),
97
+ message: error.message
98
+ }),
99
+ rejection,
100
+ plan
42
101
  };
43
102
  };
44
103
  //#endregion