@omnitend/dashboard-for-laravel 0.47.1 → 0.47.2

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.
@@ -219,7 +219,9 @@ export interface FieldDefinition {
219
219
  * For a field whose value is a list of rows (a `repeater`, or a span
220
220
  * field that renders its own rows and owns `key.N.*` error keys): the
221
221
  * row's own name, called with the row (`model[key][index]`, or `{}` when
222
- * the model has no such row) and its 0-based index. When it returns a
222
+ * the model has no such row) and its 0-based index (for a failure's row
223
+ * that has since moved, its current index; once removed, the index it
224
+ * failed at: the index "(line N)" counts). When it returns a
223
225
  * non-empty string, the error summary lists that row's errors under it
224
226
  * alone ("Chaucer Ale (Pint): Enter a price.") instead of
225
227
  * "<sub-field label> (line N)". Returning `undefined`, `null` or `""`
@@ -8,8 +8,10 @@ export interface ErrorTarget {
8
8
  * Where to take the user for this error now. The same as `errorKey`,
9
9
  * except for a row error resolved with `failedRows`: the key at the
10
10
  * failed row's CURRENT index (`lines.1.price` once row 0 is removed),
11
- * or the owning field's key when that row is gone. Always set by
12
- * `resolveErrorTargets`; optional so targets built by hand still type.
11
+ * or the owning field's key when that row is gone (removed, replaced, or
12
+ * flagged by the field's `softDeleteKey`); DXForm then focuses the
13
+ * field's container, not a row. Always set by `resolveErrorTargets`;
14
+ * optional so targets built by hand still type.
13
15
  */
14
16
  focusKey?: string;
15
17
  /** The server's messages for that key, as sent. */
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-10-21T13:01:22.201Z",
2
+ "generated": "2026-10-21T13:19:32.045Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.47.1"
5
+ "version": "0.47.2"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -2205,7 +2205,7 @@
2205
2205
  "name": "(value: any) => void"
2206
2206
  },
2207
2207
  "name": "update",
2208
- "description": "Setter that writes the value back and clears the field's validation error, and any error under `<key>.` (rows the slot draws itself)."
2208
+ "description": "Setter that writes the value back and clears the field's validation error, and every error under `<key>.` when the value is an array whose rows were added, removed, replaced or reordered (rows the slot draws itself)."
2209
2209
  }
2210
2210
  ]
2211
2211
  },
@@ -2287,7 +2287,7 @@
2287
2287
  "name": "(value: any) => void"
2288
2288
  },
2289
2289
  "name": "update",
2290
- "description": "Setter that writes the value back and clears the field's validation error, and any error under `<key>.` (rows the slot draws itself)."
2290
+ "description": "Setter that writes the value back and clears the field's validation error, and every error under `<key>.` when the value is an array whose rows were added, removed, replaced or reordered (rows the slot draws itself)."
2291
2291
  }
2292
2292
  ]
2293
2293
  },
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-10-21T13:01:22.256Z
4
+ > Last updated: 2026-10-21T13:19:32.105Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.47.1",
3
+ "version": "0.47.2",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -22,7 +22,7 @@
22
22
  @binding {FieldDefinition} field The field definition being rendered.
23
23
  @binding {any} model The model passed to field predicates (defaults to the live form data).
24
24
  @binding {any} value The current field value.
25
- @binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error, and any error under `<key>.` (rows the slot draws itself).
25
+ @binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error, and every error under `<key>.` when the value is an array whose rows were added, removed, replaced or reordered (rows the slot draws itself).
26
26
  -->
27
27
  <slot
28
28
  name="span"
@@ -93,7 +93,7 @@
93
93
  @binding {FieldDefinition} field The field definition being rendered.
94
94
  @binding {any} model The model passed to field predicates (defaults to the live form data).
95
95
  @binding {any} value The current field value.
96
- @binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error, and any error under `<key>.` (rows the slot draws itself).
96
+ @binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error, and every error under `<key>.` when the value is an array whose rows were added, removed, replaced or reordered (rows the slot draws itself).
97
97
  -->
98
98
  <slot
99
99
  v-else-if="showValueSlot && $slots.value"
@@ -420,6 +420,7 @@ import {
420
420
  onBeforeUnmount,
421
421
  provide,
422
422
  ref,
423
+ toRaw,
423
424
  useId,
424
425
  useSlots,
425
426
  watch,
@@ -744,16 +745,51 @@ function setValue(value: any): void {
744
745
  props.form.clearError(errorKey.value);
745
746
  }
746
747
 
748
+ /** The raw rows of an array value, in order, or null for any other value. */
749
+ function rowIdentitiesOf(value: unknown): unknown[] | null {
750
+ return Array.isArray(value) ? value.map((row) => toRaw(row)) : null;
751
+ }
752
+
753
+ /**
754
+ * The field's rows as last seen: refreshed by the watcher below whenever the
755
+ * array or its membership changes (not on edits inside a row), and by each
756
+ * slot `update`. The watcher runs before the next render, so a slot that
757
+ * changes the rows in place and calls `update` in the same handler is still
758
+ * compared against the rows from before the change.
759
+ */
760
+ let lastSeenRows: unknown[] | null = null;
761
+ watch(
762
+ () => rowIdentitiesOf(fieldValue.value),
763
+ (rows) => {
764
+ lastSeenRows = rows;
765
+ },
766
+ { immediate: true },
767
+ );
768
+
769
+ function haveRowsMoved(previous: unknown[] | null, next: unknown[]): boolean {
770
+ if (previous === null || previous.length !== next.length) return true;
771
+ return next.some((row, index) => row !== previous[index]);
772
+ }
773
+
747
774
  /**
748
775
  * `update` for the `span` and `value` slots (and so DXTable's `edit-span` /
749
- * `edit-value`). Such a slot may draw rows of its own and write the whole
750
- * array back, so besides the field's own error it clears every error under
751
- * `<key>.` (`lines.3.price`), as the repeater's add and remove do: the rows
752
- * may have moved, and a stale per-row error would stay in the summary under
753
- * the wrong row. They come back on the next submit.
776
+ * `edit-value`). Like a plain set, it clears the field's own error. When the
777
+ * value is an array whose rows changed (added, removed, replaced or
778
+ * reordered, compared by identity with the rows as last seen), it also
779
+ * clears every error under `<key>.` (`lines.3.price`), as the repeater's add
780
+ * and remove do: a per-row error would otherwise stay under whichever row
781
+ * now sits at its index. An update that keeps the same rows in the same
782
+ * order (a row editor writing the array back on each keystroke), or a
783
+ * value that is not an array (an `address` object), keeps the nested
784
+ * errors: they still name the right place. They come back on the next
785
+ * submit either way.
754
786
  */
755
787
  function setValueFromSlot(value: any): void {
788
+ const nextRows = rowIdentitiesOf(value);
789
+ const rowsMoved = nextRows !== null && haveRowsMoved(lastSeenRows, nextRows);
756
790
  setValue(value);
791
+ if (nextRows !== null) lastSeenRows = nextRows;
792
+ if (!rowsMoved) return;
757
793
  const prefix = `${errorKey.value}.`;
758
794
  for (const key of Object.keys(props.form.errors)) {
759
795
  if (key.startsWith(prefix)) {
@@ -1085,6 +1085,33 @@ function focusSelectedTab(): void {
1085
1085
  document.getElementById(buttonIdFor(selectedKey))?.focus();
1086
1086
  }
1087
1087
 
1088
+ /**
1089
+ * Focus a field's container rather than any control inside it: the field's
1090
+ * labelled group when it has one (a repeater's `fieldset`, announced by its
1091
+ * legend), else the marked element itself. It is made focusable for this
1092
+ * focus only (`tabindex="-1"`, removed again on blur), so the Tab order is
1093
+ * unchanged. Used when the row an error named is gone: every control inside
1094
+ * the field belongs to some other row. True once it holds focus.
1095
+ */
1096
+ function focusFieldContainer(element: HTMLElement): boolean {
1097
+ const container =
1098
+ element.querySelector<HTMLElement>(':scope > fieldset, :scope > [role="group"]') ??
1099
+ element;
1100
+ if (!container.hasAttribute("tabindex")) {
1101
+ container.setAttribute("tabindex", "-1");
1102
+ container.addEventListener(
1103
+ "blur",
1104
+ () => container.removeAttribute("tabindex"),
1105
+ { once: true },
1106
+ );
1107
+ }
1108
+ element.scrollIntoView({ block: "nearest" });
1109
+ container.focus({ preventScroll: true });
1110
+ if (document.activeElement === container) return true;
1111
+ if (container.getAttribute("tabindex") === "-1") container.removeAttribute("tabindex");
1112
+ return false;
1113
+ }
1114
+
1088
1115
  /**
1089
1116
  * Take the user to an error a summary lists: select its tab, wait for the
1090
1117
  * pane to render, look the field up in the form (its marker, anywhere
@@ -1097,7 +1124,11 @@ function focusSelectedTab(): void {
1097
1124
  * its key whenever its element leaves the DOM (a re-render replacing it),
1098
1125
  * and a moment with none at all is waited out. When no marker renders
1099
1126
  * within the field's render frames, or nothing in it takes focus, focus
1100
- * moves to the selected tab's button. Exposed so a summary rendered OUTSIDE
1127
+ * moves to the selected tab's button. When the row the error named is gone
1128
+ * (`focusKey` is the field's key while `errorKey` names something under it:
1129
+ * the row was removed, replaced or soft-deleted), the field's container
1130
+ * takes focus instead of a control, since every control in it belongs to
1131
+ * another row (`focusFieldContainer`). Exposed so a summary rendered OUTSIDE
1101
1132
  * the form (DXTable's modal footer) can drive it.
1102
1133
  */
1103
1134
  async function focusErrorTarget(target: ErrorSummarySelection): Promise<void> {
@@ -1105,6 +1136,10 @@ async function focusErrorTarget(target: ErrorSummarySelection): Promise<void> {
1105
1136
  commitTabKey(target.tabKey);
1106
1137
  }
1107
1138
  const token = ++revealToken;
1139
+ const isTargetGone =
1140
+ target.focusKey === target.fieldKey &&
1141
+ target.errorKey !== undefined &&
1142
+ target.errorKey !== target.fieldKey;
1108
1143
  let element = await renderedErrorElement(
1109
1144
  target.focusKey ?? target.errorKey ?? target.fieldKey,
1110
1145
  target.fieldKey,
@@ -1114,6 +1149,10 @@ async function focusErrorTarget(target: ErrorSummarySelection): Promise<void> {
1114
1149
  focusSelectedTab();
1115
1150
  return;
1116
1151
  }
1152
+ if (isTargetGone) {
1153
+ if (!focusFieldContainer(element)) focusSelectedTab();
1154
+ return;
1155
+ }
1117
1156
  const path = element.getAttribute("data-dx-field-key") ?? target.fieldKey;
1118
1157
  scrollFieldIntoView(element);
1119
1158
 
@@ -78,7 +78,9 @@ export interface ErrorSummarySelection {
78
78
  /**
79
79
  * Where to take the user: the error key at the failed row's current
80
80
  * index, or the field's key when that row is gone (see
81
- * `ErrorTarget.focusKey`). Equal to `errorKey` for everything else.
81
+ * `ErrorTarget.focusKey`), when DXForm's `focusErrorTarget` focuses the
82
+ * field's container rather than any row's control. Equal to `errorKey`
83
+ * for everything else.
82
84
  * Always emitted; optional so a selection built by hand for DXForm's
83
85
  * exposed `focusErrorTarget` still types (it then uses `errorKey`).
84
86
  */
@@ -27,11 +27,19 @@ export interface SubmitFailure {
27
27
  /** The validation errors the submit returned (`{}` when none). */
28
28
  errors: ValidationErrors;
29
29
  /**
30
- * The rows the errors named, as they were when the submit failed, keyed
31
- * by row path (`lines.2` for `lines.2.price`; every numeric segment of
32
- * every error key gets one). Absent when no error key names a row that
33
- * exists in the form data. The error summary uses it to keep naming
34
- * and finding the row that failed after rows are removed or reordered.
30
+ * The rows the errors named, keyed by row path: the path up to the
31
+ * key's FIRST numeric segment (`lines.2` for `lines.2.price` and for
32
+ * `lines.2.options.0.name`; a row nested inside a row is not recorded,
33
+ * since the summary follows the outer row). Absent when no error key
34
+ * names a row that exists in the form data. The error summary uses it
35
+ * to keep naming and finding the row that failed after rows are removed
36
+ * or reordered.
37
+ *
38
+ * The rows are read from the form data when the failure response
39
+ * ARRIVES, not when the request was sent. Rows added, removed or
40
+ * reordered while the request was in flight, or a submit `transform`
41
+ * that drops or reorders rows, can attach an error to the wrong row
42
+ * here, exactly as a bare index key always could.
35
43
  */
36
44
  failedRows?: Record<string, FailedRow>;
37
45
  }
@@ -249,8 +257,14 @@ function submitFailureFrom(error: unknown): SubmitFailure | null {
249
257
 
250
258
  /**
251
259
  * A copy of a row for `FailedRow.snapshot`: a deep clone when the row allows
252
- * one, else a shallow copy (a row holding a function or a `File` proxy that
253
- * `structuredClone` refuses still gets a name).
260
+ * one (`File`, `Blob` and `Date` values clone fine), else a shallow copy, so
261
+ * a row `structuredClone` refuses still gets a name. It refuses a row
262
+ * holding a function, a symbol, a DOM node or a proxy (a reactive object
263
+ * nested in the row: the row itself is raw, the objects inside it may not
264
+ * be). A class instance does NOT make it throw: it is cloned as a plain
265
+ * object, losing its prototype, getters and private fields, so a `rowLabel`
266
+ * reading a getter on such a value reads `undefined` (and falls back to the
267
+ * line form).
254
268
  */
255
269
  function snapshotRow(row: object): Record<string, any> {
256
270
  try {
@@ -262,7 +276,10 @@ function snapshotRow(row: object): Record<string, any> {
262
276
 
263
277
  /**
264
278
  * The `failedRows` for a failure's error keys, read from the form data as it
265
- * is when the failure is recorded. Undefined when no key names a row.
279
+ * is when the failure is recorded. Only the row at a key's first numeric
280
+ * segment is recorded: that is the only one the error summary reads
281
+ * (`<field key>.<index>`), so deeper rows would be cloned for nothing.
282
+ * Undefined when no key names a row.
266
283
  */
267
284
  function failedRowsFor(
268
285
  errors: ValidationErrors,
@@ -271,14 +288,15 @@ function failedRowsFor(
271
288
  const rows: Record<string, FailedRow> = {};
272
289
  for (const errorKey of Object.keys(errors)) {
273
290
  const segments = errorKey.split(".");
274
- for (let index = 1; index < segments.length; index += 1) {
275
- if (!/^\d+$/.test(segments[index])) continue;
276
- const rowPath = segments.slice(0, index + 1).join(".");
277
- if (rows[rowPath] !== undefined) continue;
278
- const row = toRaw(getByPath(data, rowPath));
279
- if (row === null || typeof row !== "object") continue;
280
- rows[rowPath] = markRaw({ row, snapshot: snapshotRow(row) });
281
- }
291
+ const firstRowSegment = segments.findIndex(
292
+ (segment, index) => index > 0 && /^\d+$/.test(segment),
293
+ );
294
+ if (firstRowSegment < 0) continue;
295
+ const rowPath = segments.slice(0, firstRowSegment + 1).join(".");
296
+ if (rows[rowPath] !== undefined) continue;
297
+ const row = toRaw(getByPath(data, rowPath));
298
+ if (row === null || typeof row !== "object") continue;
299
+ rows[rowPath] = markRaw({ row, snapshot: snapshotRow(row) });
282
300
  }
283
301
  return Object.keys(rows).length > 0 ? rows : undefined;
284
302
  }
@@ -278,7 +278,9 @@ export interface FieldDefinition {
278
278
  * For a field whose value is a list of rows (a `repeater`, or a span
279
279
  * field that renders its own rows and owns `key.N.*` error keys): the
280
280
  * row's own name, called with the row (`model[key][index]`, or `{}` when
281
- * the model has no such row) and its 0-based index. When it returns a
281
+ * the model has no such row) and its 0-based index (for a failure's row
282
+ * that has since moved, its current index; once removed, the index it
283
+ * failed at: the index "(line N)" counts). When it returns a
282
284
  * non-empty string, the error summary lists that row's errors under it
283
285
  * alone ("Chaucer Ale (Pint): Enter a price.") instead of
284
286
  * "<sub-field label> (line N)". Returning `undefined`, `null` or `""`
@@ -24,8 +24,10 @@ export interface ErrorTarget {
24
24
  * Where to take the user for this error now. The same as `errorKey`,
25
25
  * except for a row error resolved with `failedRows`: the key at the
26
26
  * failed row's CURRENT index (`lines.1.price` once row 0 is removed),
27
- * or the owning field's key when that row is gone. Always set by
28
- * `resolveErrorTargets`; optional so targets built by hand still type.
27
+ * or the owning field's key when that row is gone (removed, replaced, or
28
+ * flagged by the field's `softDeleteKey`); DXForm then focuses the
29
+ * field's container, not a row. Always set by `resolveErrorTargets`;
30
+ * optional so targets built by hand still type.
29
31
  */
30
32
  focusKey?: string;
31
33
  /** The server's messages for that key, as sent. */
@@ -288,12 +290,21 @@ export function resolveErrorTargets(
288
290
 
289
291
  /**
290
292
  * Where a failed row is now: its index in the field's live rows, found by
291
- * identity, or -1 when it is gone (removed, or the rows were replaced).
293
+ * identity, or -1 when it is gone (removed, the rows were replaced, or
294
+ * the row carries the field's `softDeleteKey` flag: DXRepeater keeps
295
+ * such a row in the array, so it is submitted, but no longer renders it).
292
296
  */
293
297
  const currentIndexOf = (field: FieldDefinition, failedRow: FailedRow): number => {
294
298
  const liveRows = getByPath(model, field.key);
295
299
  if (!Array.isArray(liveRows)) return -1;
296
- return liveRows.findIndex((row) => toRaw(row) === failedRow.row);
300
+ const index = liveRows.findIndex((row) => toRaw(row) === failedRow.row);
301
+ if (index < 0) return -1;
302
+ // Read the flag through the LIVE row (a reactive proxy in a form),
303
+ // not the raw `failedRow.row`, so a caller's computed re-resolves
304
+ // when the row is flagged.
305
+ const softDeleteKey = field.softDeleteKey;
306
+ if (softDeleteKey && Boolean(liveRows[index]?.[softDeleteKey])) return -1;
307
+ return index;
297
308
  };
298
309
 
299
310
  /**
@@ -303,7 +314,8 @@ export function resolveErrorTargets(
303
314
  * focus key uses the row's current index, or is the field's key when the
304
315
  * row is gone. "(line N)" then counts the row's CURRENT position while it
305
316
  * exists (the line a click takes the user to), and the position it failed
306
- * at once it is gone.
317
+ * at once it is gone. `rowLabel(row, index)` is given that same index, so
318
+ * a name built from the index agrees with the line form.
307
319
  */
308
320
  const ownedTarget = (
309
321
  errorKey: string,
@@ -344,7 +356,7 @@ export function resolveErrorTargets(
344
356
  const row = getByPath(model, `${field.key}.${first}`);
345
357
  rowModel = row !== null && typeof row === "object" ? row : {};
346
358
  }
347
- const rowName = rowLabelFor(field, rowModel, failedIndex);
359
+ const rowName = rowLabelFor(field, rowModel, lineIndex);
348
360
  if (rowName !== null) return { label: rowName, focusKey };
349
361
  const base = subField ? labelFor(subField, rowModel) : labelFor(field);
350
362
  return { label: `${base} (line ${lineIndex + 1})`, focusKey };