@protobase/ui 0.63.3 → 0.63.4

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 (42) hide show
  1. package/dist/activity-chart/activity-chart.js +1 -1
  2. package/dist/app/lines-plan.js +116 -0
  3. package/dist/app/list-order.js +7 -1
  4. package/dist/app/record-lines-cell.js +47 -0
  5. package/dist/app/record-lines.js +102 -25
  6. package/dist/app/record-messages.js +8 -2
  7. package/dist/app/record-page.js +13 -15
  8. package/dist/app/use-lines-draft.js +94 -35
  9. package/dist/app/use-lines.js +21 -7
  10. package/dist/app/use-record-editor.js +15 -10
  11. package/dist/app/use-record-lines.js +47 -0
  12. package/dist/data/use-list.d.ts +27 -1
  13. package/dist/data/use-list.js +10 -3
  14. package/dist/data-grid/cell-text.js +1 -1
  15. package/dist/data-grid/cells.js +1 -1
  16. package/dist/inline-child-table/columns.js +7 -2
  17. package/dist/inline-child-table/inline-child-table.js +29 -48
  18. package/dist/inline-child-table/line-row.js +16 -44
  19. package/dist/inline-child-table/messages.js +2 -0
  20. package/dist/package.js +1 -1
  21. package/dist/shell/assets/app-CBYlZ19-.js +9 -0
  22. package/dist/shell/assets/{calls-CEW8hdPi.js → calls-BIXaKTs0.js} +2 -2
  23. package/dist/shell/assets/{index-D7O0sMwH.js → index-BshKxX1Z.js} +2 -2
  24. package/dist/shell/assets/index-BvT0X9o1.css +2 -0
  25. package/dist/shell/assets/{records-map-BIDl1ull.js → records-map-D1SJKyvj.js} +2 -2
  26. package/dist/shell/assets/{shared-protobase-ui-F5elmh4N.js → shared-protobase-ui-DibYt-TM.js} +2 -2
  27. package/dist/shell/index.html +4 -4
  28. package/dist/shell/protobase.shell.json +2 -2
  29. package/dist/shell/protobase.sw.js +2 -2
  30. package/dist/shell-source-maps/assets/app-CBYlZ19-.js.map +1 -0
  31. package/dist/shell-source-maps/assets/{calls-CEW8hdPi.js.map → calls-BIXaKTs0.js.map} +1 -1
  32. package/dist/shell-source-maps/assets/{index-D7O0sMwH.js.map → index-BshKxX1Z.js.map} +1 -1
  33. package/dist/shell-source-maps/assets/{records-map-BIDl1ull.js.map → records-map-D1SJKyvj.js.map} +1 -1
  34. package/dist/shell-source-maps/assets/{shared-protobase-ui-F5elmh4N.js.map → shared-protobase-ui-DibYt-TM.js.map} +1 -1
  35. package/package.json +4 -4
  36. package/dist/inline-child-table/number-cell.js +0 -25
  37. package/dist/inline-child-table/totals-footer.js +0 -39
  38. package/dist/mocks/invoices.js +0 -13
  39. package/dist/mocks/products.js +0 -167
  40. package/dist/shell/assets/app-Bm6h0l5D.js +0 -9
  41. package/dist/shell/assets/index-Be8nCyRW.css +0 -2
  42. package/dist/shell-source-maps/assets/app-Bm6h0l5D.js.map +0 -1
@@ -15,7 +15,7 @@ const without = (source, name) => Object.fromEntries(Object.entries(source).filt
15
15
  /**
16
16
  * Draft state for one record. Saving sends only the changed fields with the ETag that was read;
17
17
  * a 412 refetches the record and marks the fields both sides changed so the user can pick a side.
18
- * When the lines were reordered too, Save sends the record and the new order as one batch: all or nothing.
18
+ * When the lines changed too, Save sends the record and the lines' writes as one batch: all or nothing.
19
19
  */
20
20
  const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, lines }) => {
21
21
  const client = useClient();
@@ -38,7 +38,7 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
38
38
  useEffect(() => {
39
39
  if (!draftKey) return;
40
40
  const kept = Object.fromEntries(Object.entries(draft).filter(([name]) => !model.fields[name]?.sensitive));
41
- const keep = Object.keys(kept).length > 0 || Boolean(lines?.dirty);
41
+ const keep = Object.keys(kept).length > 0 || Boolean(lines?.order);
42
42
  writeDraft(localStorage, draftKey, keep ? {
43
43
  etag: stored.etag,
44
44
  fields: kept,
@@ -48,7 +48,6 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
48
48
  draftKey,
49
49
  model,
50
50
  draft,
51
- lines?.dirty,
52
51
  lines?.order,
53
52
  stored.etag
54
53
  ]);
@@ -93,14 +92,15 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
93
92
  throw error;
94
93
  }
95
94
  };
96
- const saveBatch = async (linesOps) => {
97
- const ops = [...fieldsDirty ? [{
95
+ const saveBatch = async (linesPlan) => {
96
+ const recordOps = fieldsDirty ? [{
98
97
  op: "update",
99
98
  resource: model.name,
100
99
  key: recordKey,
101
100
  data: patchOf(),
102
101
  etag: stored.etag
103
- }] : [], ...linesOps];
102
+ }] : [];
103
+ const ops = [...recordOps, ...linesPlan.ops];
104
104
  try {
105
105
  await client.batchWrite(ops);
106
106
  } catch (error) {
@@ -108,7 +108,12 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
108
108
  await adoptLatest();
109
109
  await queryClient.invalidateQueries({ queryKey: ["list"] });
110
110
  setLinesConflict(true);
111
- } else if (isApiError(error)) reject(error);
111
+ } else if (isApiError(error)) {
112
+ const lineOp = error.operation === void 0 ? -1 : error.operation - recordOps.length;
113
+ const row = linesPlan.rows[lineOp];
114
+ if (lineOp < 0) reject(error);
115
+ else if (row) lines?.reject(row, error);
116
+ }
112
117
  throw isApiError(error) ? nameFailingOperation(error, ops) : error;
113
118
  }
114
119
  await Promise.all([
@@ -120,8 +125,8 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
120
125
  lines?.reset();
121
126
  };
122
127
  const save = () => feedback.run(async () => {
123
- const linesOps = lines?.op() ?? [];
124
- if (linesOps.length > 0) await saveBatch(linesOps);
128
+ const linesPlan = lines?.plan();
129
+ if (linesPlan && linesPlan.ops.length > 0) await saveBatch(linesPlan);
125
130
  else await saveRecordOnly();
126
131
  setDraft({});
127
132
  setConflicts({});
@@ -146,7 +151,7 @@ const useRecordEditor = ({ model, stored, recordKey, mode, subject, draftKey, li
146
151
  /** The changes on screen were brought back from this browser. */
147
152
  restored: showRestored && dirty,
148
153
  dismissRestored: () => setShowRestored(false),
149
- /** Someone changed the lines while the new order was being made. */
154
+ /** Someone changed the lines while they were being edited here. */
150
155
  linesConflict,
151
156
  saving: feedback.phase === "saving",
152
157
  phase: feedback.phase,
@@ -0,0 +1,47 @@
1
+ import { useAdminMeta } from "./meta-gate.js";
2
+ import { listColumnNames } from "../live/columns-from-model.js";
3
+ import { keyValue } from "./create/create-values.js";
4
+ import { listLabels } from "../live/relation-labels.js";
5
+ import { useLines } from "./use-lines.js";
6
+ import { useLinesDraft } from "./use-lines-draft.js";
7
+ import { useMemo } from "react";
8
+ //#region src/app/use-record-lines.ts
9
+ /** A resource that holds numbered lines of this one: it points at the parent and has `position` and `quantity`. */
10
+ 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));
11
+ /**
12
+ * The lines' columns: the list columns of their view, or its first fields, without what the editor fills in itself:
13
+ * the key, the tenant, the relation to the record and the position, which is the order of the rows.
14
+ */
15
+ const lineColumnNames = (lines, view, link) => listColumnNames(lines, view).filter((name) => {
16
+ const field = lines.fields[name];
17
+ return field && !field.writeOnly && name !== link && name !== "position" && name !== lines.tenant && !lines.primaryKey.includes(name);
18
+ });
19
+ /** The record's lines when it has a lines resource: the resource, its columns, the lines as stored and the changes made to them. */
20
+ const useRecordLines = (parent, parentKey) => {
21
+ const { resources, views } = useAdminMeta();
22
+ const model = useMemo(() => findLinesResource(resources, parent), [resources, parent]);
23
+ const link = model && Object.values(model.fields).find((field) => field.type === "relation" && field.relation?.resource === parent.name);
24
+ const columns = model && link ? lineColumnNames(model, views[model.name], link.name) : [];
25
+ const data = useLines(model, link, parentKey, model ? listLabels(resources, views, model, columns) : void 0);
26
+ const target = useMemo(() => model && link ? {
27
+ model,
28
+ position: "position",
29
+ link: {
30
+ field: link.name,
31
+ value: keyValue(parent, parentKey)
32
+ }
33
+ } : void 0, [
34
+ model,
35
+ link,
36
+ parent,
37
+ parentKey
38
+ ]);
39
+ return {
40
+ model,
41
+ columns,
42
+ data,
43
+ draft: useLinesDraft(target, data.items)
44
+ };
45
+ };
46
+ //#endregion
47
+ export { findLinesResource, lineColumnNames, useRecordLines };
@@ -6,6 +6,8 @@ export type UseListParams = {
6
6
  orderBy?: string;
7
7
  pageSize?: number;
8
8
  fields?: string[];
9
+ /** Names the records relations point at, as `client.list`'s `labels`: `{ product_id: 'name' }`. */
10
+ labels?: Record<string, string>;
9
11
  /** Start at this page token instead of the first page, for example one from `useAnchors`. */
10
12
  startToken?: string;
11
13
  enabled?: boolean;
@@ -14,9 +16,13 @@ export type UseListParams = {
14
16
  };
15
17
  type Row = Record<string, unknown>;
16
18
  /** Infinite pages of a resource, following `next_page_token`. */
17
- export declare const useList: (resource: string, { filter, orderBy, pageSize, fields, startToken, enabled, refetchInterval }?: UseListParams) => {
19
+ export declare const useList: (resource: string, { filter, orderBy, pageSize, fields, labels, startToken, enabled, refetchInterval }?: UseListParams) => {
18
20
  items: Row[];
19
21
  totalEstimate: number | undefined;
22
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
23
+ labels: {
24
+ [k: string]: Record<string, string>;
25
+ };
20
26
  data: InfiniteData<ListPage<Row>, unknown>;
21
27
  error: Error;
22
28
  isError: true;
@@ -53,6 +59,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
53
59
  } | {
54
60
  items: Row[];
55
61
  totalEstimate: number | undefined;
62
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
63
+ labels: {
64
+ [k: string]: Record<string, string>;
65
+ };
56
66
  data: InfiniteData<ListPage<Row>, unknown>;
57
67
  error: null;
58
68
  isError: false;
@@ -89,6 +99,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
89
99
  } | {
90
100
  items: Row[];
91
101
  totalEstimate: number | undefined;
102
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
103
+ labels: {
104
+ [k: string]: Record<string, string>;
105
+ };
92
106
  data: undefined;
93
107
  error: Error;
94
108
  isError: true;
@@ -125,6 +139,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
125
139
  } | {
126
140
  items: Row[];
127
141
  totalEstimate: number | undefined;
142
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
143
+ labels: {
144
+ [k: string]: Record<string, string>;
145
+ };
128
146
  data: undefined;
129
147
  error: null;
130
148
  isError: false;
@@ -161,6 +179,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
161
179
  } | {
162
180
  items: Row[];
163
181
  totalEstimate: number | undefined;
182
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
183
+ labels: {
184
+ [k: string]: Record<string, string>;
185
+ };
164
186
  data: undefined;
165
187
  error: null;
166
188
  isError: false;
@@ -197,6 +219,10 @@ export declare const useList: (resource: string, { filter, orderBy, pageSize, fi
197
219
  } | {
198
220
  items: Row[];
199
221
  totalEstimate: number | undefined;
222
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
223
+ labels: {
224
+ [k: string]: Record<string, string>;
225
+ };
200
226
  data: InfiniteData<ListPage<Row>, unknown>;
201
227
  isError: false;
202
228
  error: null;
@@ -4,11 +4,15 @@ import { filterText } from "@protobase/client";
4
4
  import { useInfiniteQuery } from "@tanstack/react-query";
5
5
  //#region src/data/use-list.ts
6
6
  /** Infinite pages of a resource, following `next_page_token`. */
7
- const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken = "", enabled = true, refetchInterval } = {}) => {
7
+ const useList = (resource, { filter, orderBy, pageSize = 50, fields, labels, startToken = "", enabled = true, refetchInterval } = {}) => {
8
8
  const client = useClient();
9
9
  const text = filterText(filter) ?? "";
10
10
  const query = useInfiniteQuery({
11
- queryKey: [...keys.list(resource, text, orderBy ?? "", pageSize, startToken), fields?.join(",") ?? ""],
11
+ queryKey: [
12
+ ...keys.list(resource, text, orderBy ?? "", pageSize, startToken),
13
+ fields?.join(",") ?? "",
14
+ ...labels ? [labels] : []
15
+ ],
12
16
  enabled,
13
17
  refetchInterval,
14
18
  initialPageParam: startToken,
@@ -17,6 +21,7 @@ const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken
17
21
  orderBy,
18
22
  pageSize,
19
23
  fields,
24
+ labels,
20
25
  pageToken: pageParam
21
26
  }),
22
27
  getNextPageParam: (last) => last.nextPageToken || void 0
@@ -25,7 +30,9 @@ const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken
25
30
  return {
26
31
  ...query,
27
32
  items: pages.flatMap((page) => page.items),
28
- totalEstimate: pages[0]?.totalSizeEstimate
33
+ totalEstimate: pages[0]?.totalSizeEstimate,
34
+ /** Per relation asked for in `labels`, the names of the records it points at, from every page loaded. */
35
+ labels: Object.fromEntries(Object.keys(labels ?? {}).map((name) => [name, Object.assign({}, ...pages.map((page) => page.labels?.[name] ?? {}))]))
29
36
  };
30
37
  };
31
38
  //#endregion
@@ -1,7 +1,7 @@
1
- import { userById } from "../mocks/users.js";
2
1
  import { formatMoney } from "../format/money.js";
3
2
  import { formatInt, formatPercent, formatSigned } from "../format/number.js";
4
3
  import { formatDate, formatDateTime, formatRelativeTime } from "../format/date.js";
4
+ import { userById } from "../mocks/users.js";
5
5
  import { fileShown } from "../live/file-values.js";
6
6
  import { text } from "./messages.js";
7
7
  import { enumLabel } from "@protobase/schema";
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { userById } from "../mocks/users.js";
3
2
  import { viewerZone, zoneLabel } from "../format/zone.js";
4
3
  import { formatDateTime } from "../format/date.js";
4
+ import { userById } from "../mocks/users.js";
5
5
  import { Avatar } from "../primitives/avatar.js";
6
6
  import { Badge } from "../primitives/badge.js";
7
7
  import { fileShown, shownImage } from "../live/file-values.js";
@@ -1,4 +1,9 @@
1
1
  //#region src/inline-child-table/columns.ts
2
- const lineGrid = "grid grid-cols-[28px_minmax(0,1fr)_88px_112px_112px_32px] items-center gap-2 pointer-coarse:grid-cols-[44px_minmax(0,1fr)_88px_112px_112px_44px]";
2
+ const track = (column) => column.width ?? "minmax(8rem,1fr)";
3
+ /** The columns' tracks, between the drag handle and the menu that `lineGrid` places around them. */
4
+ const lineGridStyle = (columns) => ({ "--line-columns": columns.map(track).join(" ") });
5
+ /** The narrowest the table gets before it scrolls sideways: each column's least width, and the handle and menu. */
6
+ const lineTableMinWidth = (columns) => `${columns.reduce((sum, column) => sum + (column.width?.endsWith("px") ? parseInt(column.width) / 16 : 8), 6)}rem`;
7
+ const lineGrid = "grid grid-cols-[28px_var(--line-columns)_32px] items-center gap-2 pointer-coarse:grid-cols-[44px_var(--line-columns)_44px]";
3
8
  //#endregion
4
- export { lineGrid };
9
+ export { lineGrid, lineGridStyle, lineTableMinWidth };
@@ -1,18 +1,20 @@
1
+ import { cn } from "../lib/cn.js";
1
2
  import { Button } from "../primitives/button.js";
2
- import { productAt } from "../mocks/products.js";
3
- import { lineGrid } from "./columns.js";
3
+ import { lineGrid, lineGridStyle } from "./columns.js";
4
4
  import { text } from "./messages.js";
5
5
  import { LineRow } from "./line-row.js";
6
6
  import { reorder } from "./reorder.js";
7
- import { TotalsFooter } from "./totals-footer.js";
8
7
  import { Plus } from "lucide-react";
9
8
  import { jsx, jsxs } from "react/jsx-runtime";
10
9
  import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
11
10
  import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable";
12
11
  //#region src/inline-child-table/inline-child-table.tsx
13
12
  const headerCell = "px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground";
14
- const announcementsFor = (lines) => {
15
- const name = (id) => lines.find((line) => line.id === id)?.description ?? text("line");
13
+ const announcementsFor = (lines, lineName) => {
14
+ const name = (id) => {
15
+ const line = lines.find((each) => each.id === id);
16
+ return line ? lineName(line) : text("line");
17
+ };
16
18
  const place = (id) => lines.findIndex((line) => line.id === id) + 1;
17
19
  return {
18
20
  onDragStart: ({ active }) => text("pickedUp", name(active.id), place(active.id), lines.length),
@@ -21,15 +23,16 @@ const announcementsFor = (lines) => {
21
23
  onDragCancel: ({ active }) => text("cancelled", name(active.id), place(active.id))
22
24
  };
23
25
  };
24
- const InlineChildTable = ({ lines, onChange, onReorder, vatRate = .21, readOnly, reorderable = !readOnly }) => {
26
+ /** The lines of a record as rows of the given columns, put in order by dragging or from each row's menu. */
27
+ const InlineChildTable = ({ lines, columns, lineName, onReorder, reorderable = Boolean(onReorder), onAdd, onRemove, removable = () => true, lineError, footer }) => {
25
28
  const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, { activationConstraint: {
26
29
  delay: 250,
27
30
  tolerance: 5
28
31
  } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
29
32
  const move = (from, to) => {
30
33
  const line = lines[from];
31
- if (!line || to < 0 || to >= lines.length || from === to) return;
32
- (onReorder ?? ((next) => onChange(next)))(reorder(lines, from, to), {
34
+ if (!line || !onReorder || to < 0 || to >= lines.length || from === to) return;
35
+ onReorder(reorder(lines, from, to), {
33
36
  id: line.id,
34
37
  from,
35
38
  to
@@ -39,48 +42,31 @@ const InlineChildTable = ({ lines, onChange, onReorder, vatRate = .21, readOnly,
39
42
  if (!over || active.id === over.id) return;
40
43
  move(lines.findIndex((line) => line.id === active.id), lines.findIndex((line) => line.id === over.id));
41
44
  };
42
- const addLine = () => {
43
- const product = productAt(lines.length * 3 + 1);
44
- onChange([...lines, {
45
- id: `new-${lines.length}-${product.id}`,
46
- productId: product.id,
47
- description: product.name,
48
- quantity: 1,
49
- unitPriceCents: product.priceCents
50
- }]);
51
- };
52
45
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
53
46
  className: "overflow-hidden rounded-lg border border-border bg-background",
47
+ style: lineGridStyle(columns),
54
48
  children: [
55
49
  /* @__PURE__ */ jsxs("div", {
56
50
  className: `${lineGrid} border-b border-border bg-surface px-2 py-2`,
57
51
  children: [
58
52
  /* @__PURE__ */ jsx("span", {}),
59
- /* @__PURE__ */ jsx("span", {
60
- className: headerCell,
61
- children: text("item")
62
- }),
63
- /* @__PURE__ */ jsx("span", {
64
- className: `${headerCell} text-right`,
65
- children: text("qty")
66
- }),
67
- /* @__PURE__ */ jsx("span", {
68
- className: `${headerCell} text-right`,
69
- children: text("unitPrice")
70
- }),
71
- /* @__PURE__ */ jsx("span", {
72
- className: `${headerCell} text-right`,
73
- children: text("lineTotal")
74
- }),
53
+ columns.map((column) => /* @__PURE__ */ jsx("span", {
54
+ className: cn(headerCell, column.align === "right" && "text-right"),
55
+ children: column.label
56
+ }, column.key)),
75
57
  /* @__PURE__ */ jsx("span", {})
76
58
  ]
77
59
  }),
60
+ lines.length === 0 && /* @__PURE__ */ jsx("p", {
61
+ className: "px-4 py-3 text-muted-foreground",
62
+ children: text("noLines")
63
+ }),
78
64
  /* @__PURE__ */ jsx(DndContext, {
79
65
  sensors,
80
66
  collisionDetection: closestCenter,
81
67
  onDragEnd,
82
68
  accessibility: {
83
- announcements: announcementsFor(lines),
69
+ announcements: announcementsFor(lines, lineName),
84
70
  screenReaderInstructions: { draggable: text("instructions") }
85
71
  },
86
72
  children: /* @__PURE__ */ jsx(SortableContext, {
@@ -88,33 +74,28 @@ const InlineChildTable = ({ lines, onChange, onReorder, vatRate = .21, readOnly,
88
74
  strategy: verticalListSortingStrategy,
89
75
  children: lines.map((line, index) => /* @__PURE__ */ jsx(LineRow, {
90
76
  line,
77
+ name: lineName(line),
78
+ columns,
91
79
  position: index,
92
80
  count: lines.length,
93
81
  reorderable,
94
- editable: !readOnly,
95
- onChange: (patch) => onChange(lines.map((item) => item.id === line.id ? {
96
- ...item,
97
- ...patch
98
- } : item)),
99
- onRemove: () => onChange(lines.filter((item) => item.id !== line.id)),
100
- onMove: (to) => move(index, to)
82
+ onRemove: onRemove && removable(line) ? () => onRemove(line) : void 0,
83
+ onMove: (to) => move(index, to),
84
+ error: lineError?.(line)
101
85
  }, line.id))
102
86
  })
103
87
  }),
104
- !readOnly && /* @__PURE__ */ jsx("div", {
88
+ onAdd && /* @__PURE__ */ jsx("div", {
105
89
  className: "border-t border-border bg-surface px-2 py-1.5",
106
90
  children: /* @__PURE__ */ jsxs(Button, {
107
91
  variant: "ghost",
108
92
  size: "sm",
109
- onClick: addLine,
93
+ onClick: onAdd,
110
94
  children: [/* @__PURE__ */ jsx(Plus, { className: "size-3.5" }), text("addLine")]
111
95
  })
112
96
  })
113
97
  ]
114
- }), /* @__PURE__ */ jsx(TotalsFooter, {
115
- lines,
116
- vatRate
117
- })] });
98
+ }), footer] });
118
99
  };
119
100
  //#endregion
120
101
  export { InlineChildTable };
@@ -1,16 +1,12 @@
1
1
  import { cn } from "../lib/cn.js";
2
- import { lineTotalCents } from "../mocks/invoices.js";
3
2
  import { lineGrid } from "./columns.js";
4
- import { formatMoney } from "../format/money.js";
5
3
  import { LineMenu } from "./line-menu.js";
6
- import { NumberCell } from "./number-cell.js";
7
4
  import { text } from "./messages.js";
8
5
  import { GripVertical } from "lucide-react";
9
6
  import { jsx, jsxs } from "react/jsx-runtime";
10
7
  import { useSortable } from "@dnd-kit/sortable";
11
8
  //#region src/inline-child-table/line-row.tsx
12
- const parseEuros = (typed) => Math.round(Number(typed.replace(",", ".").replace(/[^0-9.]/g, "")) * 100);
13
- const LineRow = ({ line, position, count, reorderable, editable, onChange, onRemove, onMove }) => {
9
+ const LineRow = ({ line, name, columns, position, count, reorderable, onRemove, onMove, error }) => {
14
10
  const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
15
11
  id: line.id,
16
12
  disabled: !reorderable
@@ -23,7 +19,7 @@ const LineRow = ({ line, position, count, reorderable, editable, onChange, onRem
23
19
  label: text("moveDown"),
24
20
  disabled: position === count - 1,
25
21
  onSelect: () => onMove(position + 1)
26
- }] : [], ...editable ? [{
22
+ }] : [], ...onRemove ? [{
27
23
  label: text("removeLine"),
28
24
  destructive: true,
29
25
  onSelect: onRemove
@@ -31,60 +27,36 @@ const LineRow = ({ line, position, count, reorderable, editable, onChange, onRem
31
27
  return /* @__PURE__ */ jsxs("div", {
32
28
  ref: setNodeRef,
33
29
  "data-line-id": line.id,
30
+ "aria-invalid": error ? true : void 0,
34
31
  style: {
35
32
  transform: transform ? `translate3d(0, ${transform.y}px, 0)` : void 0,
36
33
  transition
37
34
  },
38
- className: cn("group relative border-b border-border bg-background px-2 py-1 last:border-b-0 hover:bg-surface", lineGrid, isDragging && "z-10 bg-surface opacity-80 shadow-pop"),
35
+ className: cn("group relative border-b border-border bg-background px-2 py-1 last:border-b-0 hover:bg-surface", lineGrid, isDragging && "z-10 bg-surface opacity-80 shadow-pop", error && "bg-danger-soft"),
39
36
  children: [
40
37
  reorderable ? /* @__PURE__ */ jsx("button", {
41
38
  ref: setActivatorNodeRef,
42
39
  type: "button",
43
- "aria-label": text("dragToReorder", line.description),
40
+ "aria-label": text("dragToReorder", name),
44
41
  ...attributes,
45
42
  ...listeners,
46
43
  style: { touchAction: "none" },
47
44
  className: "flex size-7 cursor-grab items-center justify-center rounded text-faint-foreground opacity-60 hover:bg-muted hover:opacity-100 focus-visible:opacity-100 active:cursor-grabbing pointer-coarse:size-11",
48
45
  children: /* @__PURE__ */ jsx(GripVertical, { className: "size-4" })
49
46
  }) : /* @__PURE__ */ jsx("span", {}),
50
- /* @__PURE__ */ jsxs("div", {
51
- className: "min-w-0",
52
- children: [/* @__PURE__ */ jsx("p", {
53
- className: "truncate text-foreground",
54
- children: line.description
55
- }), /* @__PURE__ */ jsx("p", {
56
- className: "font-mono text-[11px] text-faint-foreground",
57
- children: line.productId
58
- })]
59
- }),
60
- editable ? /* @__PURE__ */ jsx(NumberCell, {
61
- label: text("quantity"),
62
- value: line.quantity,
63
- format: String,
64
- parse: (typed) => Math.round(Number(typed)),
65
- onCommit: (quantity) => onChange({ quantity })
66
- }) : /* @__PURE__ */ jsx("span", {
67
- className: "px-2 text-right tabular-nums",
68
- children: line.quantity
69
- }),
70
- editable ? /* @__PURE__ */ jsx(NumberCell, {
71
- label: text("unitPrice"),
72
- value: line.unitPriceCents,
73
- format: (cents) => (cents / 100).toFixed(2),
74
- parse: parseEuros,
75
- onCommit: (unitPriceCents) => onChange({ unitPriceCents })
76
- }) : /* @__PURE__ */ jsx("span", {
77
- className: "px-2 text-right tabular-nums",
78
- children: (line.unitPriceCents / 100).toFixed(2)
79
- }),
80
- /* @__PURE__ */ jsx("span", {
81
- className: "px-2 text-right font-medium tabular-nums text-foreground",
82
- children: formatMoney(lineTotalCents(line))
83
- }),
47
+ columns.map((column) => /* @__PURE__ */ jsx("div", {
48
+ className: cn("min-w-0", column.align === "right" && "text-right"),
49
+ children: column.cell(line)
50
+ }, column.key)),
84
51
  menu.length > 0 ? /* @__PURE__ */ jsx(LineMenu, {
85
- label: text("actionsFor", line.description),
52
+ label: text("actionsFor", name),
86
53
  items: menu
87
- }) : /* @__PURE__ */ jsx("span", {})
54
+ }) : /* @__PURE__ */ jsx("span", {}),
55
+ error && /* @__PURE__ */ jsx("p", {
56
+ role: "alert",
57
+ className: "col-span-full px-2 pb-1 text-xs text-danger-text",
58
+ children: error
59
+ })
88
60
  ]
89
61
  });
90
62
  };
@@ -15,6 +15,7 @@ const text = defineMessages({
15
15
  unitPrice: "Unit price",
16
16
  lineTotal: "Line total",
17
17
  addLine: "Add line",
18
+ noLines: "No lines yet.",
18
19
  moveUp: "Move up",
19
20
  moveDown: "Move down",
20
21
  removeLine: "Remove line",
@@ -38,6 +39,7 @@ const text = defineMessages({
38
39
  unitPrice: "Stukprijs",
39
40
  lineTotal: "Regeltotaal",
40
41
  addLine: "Regel toevoegen",
42
+ noLines: "Nog geen regels.",
41
43
  moveUp: "Omhoog",
42
44
  moveDown: "Omlaag",
43
45
  removeLine: "Regel verwijderen",
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.63.3";
2
+ var version = "0.63.4";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };