@omnitend/dashboard-for-laravel 0.47.0 → 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.
@@ -7,8 +7,9 @@
7
7
  * `when`/`context`-aware predicates and a standalone summary can use the
8
8
  * defaults below.
9
9
  */
10
+ import { toRaw } from "vue";
10
11
  import type { FieldDefinition, FormTab } from "../types";
11
- import type { ValidationErrors } from "../composables/useForm";
12
+ import type { FailedRow, ValidationErrors } from "../composables/useForm";
12
13
  import {
13
14
  isFieldVisible as isFieldVisibleFor,
14
15
  resolvePredicate,
@@ -19,6 +20,16 @@ import { getByPath } from "./objectPath";
19
20
  export interface ErrorTarget {
20
21
  /** The key as the server sent it (`lines.0.price`). */
21
22
  errorKey: string;
23
+ /**
24
+ * Where to take the user for this error now. The same as `errorKey`,
25
+ * except for a row error resolved with `failedRows`: the key at the
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 (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.
31
+ */
32
+ focusKey?: string;
22
33
  /** The server's messages for that key, as sent. */
23
34
  messages: string[];
24
35
  /**
@@ -65,6 +76,14 @@ export interface ResolveErrorTargetsOptions {
65
76
  field: FieldDefinition,
66
77
  labelModel: Record<string, any>,
67
78
  ) => string | null | undefined;
79
+ /**
80
+ * The rows a failed submit's errors named, as captured then
81
+ * (`SubmitFailure.failedRows`). Pass it when resolving that failure's
82
+ * errors after the form may have changed (the error summary does), so a
83
+ * row error keeps the name of the row that failed and points at where
84
+ * that row is now. Leave it out for the form's live errors.
85
+ */
86
+ failedRows?: Record<string, FailedRow>;
68
87
  }
69
88
 
70
89
  interface Candidate {
@@ -175,7 +194,10 @@ function rowLabelFor(
175
194
  * Labels: the owning field's label; for `field.N.sub` the sub-field's label
176
195
  * (when the field defines `fields` and one has that key) or the field's own,
177
196
  * plus "(line N+1)" — unless the field's `rowLabel` names that row, when
178
- * the row's name is the whole label; for any other nested key (a media map's
197
+ * the row's name is the whole label. With `failedRows`, a row key's label
198
+ * reads the row as it was when the submit failed, and its `focusKey` and
199
+ * line number follow that row to where it is now (see `ownedTarget`); for
200
+ * any other nested key (a media map's
179
201
  * `image_media.<uuid>`) the field's label. An unowned key is humanised.
180
202
  *
181
203
  * Display lines: "Label: message", except that the label is left off when
@@ -266,22 +288,78 @@ export function resolveErrorTargets(
266
288
  return longest;
267
289
  };
268
290
 
269
- const ownedLabel = (errorKey: string, field: FieldDefinition): string => {
270
- if (!isNestedUnder(errorKey, field.key)) return labelFor(field);
271
- const [first, second] = errorKey.slice(field.key.length + 1).split(".");
272
- if (!NUMERIC_SEGMENT.test(first)) return labelFor(field);
291
+ /**
292
+ * Where a failed row is now: its index in the field's live rows, found by
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).
296
+ */
297
+ const currentIndexOf = (field: FieldDefinition, failedRow: FailedRow): number => {
298
+ const liveRows = getByPath(model, field.key);
299
+ if (!Array.isArray(liveRows)) return -1;
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;
308
+ };
309
+
310
+ /**
311
+ * The label and focus key for an owned error key. For a row key
312
+ * (`field.N.sub`) with a captured failed row, both follow that row: the
313
+ * label is read from the row as it was when the submit failed, and the
314
+ * focus key uses the row's current index, or is the field's key when the
315
+ * row is gone. "(line N)" then counts the row's CURRENT position while it
316
+ * exists (the line a click takes the user to), and the position it failed
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.
319
+ */
320
+ const ownedTarget = (
321
+ errorKey: string,
322
+ field: FieldDefinition,
323
+ ): { label: string; focusKey: string } => {
324
+ if (!isNestedUnder(errorKey, field.key)) {
325
+ return { label: labelFor(field), focusKey: errorKey };
326
+ }
327
+ const [first, second, ...deeper] = errorKey
328
+ .slice(field.key.length + 1)
329
+ .split(".");
330
+ if (!NUMERIC_SEGMENT.test(first)) {
331
+ return { label: labelFor(field), focusKey: errorKey };
332
+ }
333
+ const failedIndex = Number(first);
273
334
  const subField =
274
335
  second !== undefined
275
336
  ? field.fields?.find((candidate) => candidate.key === second)
276
337
  : undefined;
277
- // DXRepeater renders a sub-field with the ROW as its model, so its
278
- // label resolves against the row here too.
279
- const row = getByPath(model, `${field.key}.${first}`);
280
- const rowModel = row !== null && typeof row === "object" ? row : {};
281
- const rowName = rowLabelFor(field, rowModel, Number(first));
282
- if (rowName !== null) return rowName;
338
+ const failedRow = options.failedRows?.[`${field.key}.${first}`];
339
+
340
+ let rowModel: Record<string, any>;
341
+ let lineIndex = failedIndex;
342
+ let focusKey = errorKey;
343
+ if (failedRow !== undefined) {
344
+ rowModel = failedRow.snapshot;
345
+ const currentIndex = currentIndexOf(field, failedRow);
346
+ if (currentIndex >= 0) {
347
+ lineIndex = currentIndex;
348
+ const rest = second !== undefined ? [second, ...deeper] : [];
349
+ focusKey = [field.key, String(currentIndex), ...rest].join(".");
350
+ } else {
351
+ focusKey = field.key;
352
+ }
353
+ } else {
354
+ // DXRepeater renders a sub-field with the ROW as its model, so its
355
+ // label resolves against the row here too.
356
+ const row = getByPath(model, `${field.key}.${first}`);
357
+ rowModel = row !== null && typeof row === "object" ? row : {};
358
+ }
359
+ const rowName = rowLabelFor(field, rowModel, lineIndex);
360
+ if (rowName !== null) return { label: rowName, focusKey };
283
361
  const base = subField ? labelFor(subField, rowModel) : labelFor(field);
284
- return `${base} (line ${Number(first) + 1})`;
362
+ return { label: `${base} (line ${lineIndex + 1})`, focusKey };
285
363
  };
286
364
 
287
365
  // ——— one target per error key with at least one message
@@ -298,15 +376,15 @@ export function resolveErrorTargets(
298
376
  );
299
377
  if (messages.length === 0) return;
300
378
  const owner = ownerOf(errorKey);
379
+ const owned = owner ? ownedTarget(errorKey, owner.field) : null;
301
380
  pending.push({
302
381
  target: {
303
382
  errorKey,
383
+ focusKey: owned ? owned.focusKey : errorKey,
304
384
  messages,
305
385
  fieldKey: owner ? owner.field.key : null,
306
386
  tabKey: owner ? owner.tabKey : null,
307
- label: owner
308
- ? ownedLabel(errorKey, owner.field)
309
- : humaniseErrorKey(errorKey),
387
+ label: owned ? owned.label : humaniseErrorKey(errorKey),
310
388
  },
311
389
  rank: owner ? owner.rank : [UNOWNED_RANK, UNOWNED_RANK],
312
390
  order,