@omnitend/dashboard-for-laravel 0.16.1 → 0.16.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.
@@ -136,6 +136,18 @@ export interface FieldDefinition {
136
136
  /** Minimum / maximum number of repeater rows. */
137
137
  minItems?: number;
138
138
  maxItems?: number;
139
+ /**
140
+ * For `repeater` fields backed by an upsert-children API contract (rows
141
+ * carrying a persisted `id` are only deleted server-side when submitted
142
+ * flagged, e.g. Laravel's `{ id, to_delete: true }` pattern): the
143
+ * sub-field key to set `true` when a row is removed, instead of splicing
144
+ * it out of the array. Only applies to rows that already have an `id`
145
+ * (freshly added, never-persisted rows are still spliced normally since
146
+ * the server has never seen them). Flagged rows remain in `form.data`
147
+ * (so they're submitted) but are hidden from the UI and excluded from
148
+ * `minItems`/`maxItems` counts.
149
+ */
150
+ softDeleteKey?: string;
139
151
  /** Disable the field (static or computed from the model). */
140
152
  disabled?: MaybeFn<boolean>;
141
153
  /**
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-07-05T08:43:52.804Z",
2
+ "generated": "2026-07-06T07:46:59.742Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.15.0"
5
+ "version": "0.16.1"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -2146,7 +2146,7 @@
2146
2146
  "name": "number"
2147
2147
  },
2148
2148
  "name": "index",
2149
- "description": "The row's zero-based index."
2149
+ "description": "The row's zero-based position among visible rows."
2150
2150
  },
2151
2151
  {
2152
2152
  "title": "binding",
@@ -2162,7 +2162,7 @@
2162
2162
  "name": "function"
2163
2163
  },
2164
2164
  "name": "remove",
2165
- "description": "Removes this row (respects `minItems`)."
2165
+ "description": "Removes this row (respects `minItems`; soft-deletes instead of splicing when `field.softDeleteKey` is set and the row has an `id`)."
2166
2166
  },
2167
2167
  {
2168
2168
  "title": "binding",
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-05T08:43:52.857Z
4
+ > Last updated: 2026-07-06T07:46:59.797Z
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.16.1",
3
+ "version": "0.16.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",
@@ -474,6 +474,7 @@ function setValue(value: any): void {
474
474
  } else {
475
475
  (props.form.data as Record<string, any>)[props.field.key] = next;
476
476
  }
477
+ props.form.touched[errorKey.value] = true;
477
478
  props.form.clearError(errorKey.value);
478
479
  }
479
480
 
@@ -7,37 +7,37 @@
7
7
  <template>
8
8
  <div class="dx-repeater">
9
9
  <div
10
- v-for="(row, index) in rows"
11
- :key="rowKey(index)"
10
+ v-for="(entry, position) in visibleRows"
11
+ :key="rowKey(entry.row, position)"
12
12
  class="dx-repeater-row"
13
13
  >
14
14
  <!--
15
15
  @slot Custom layout for a single repeater row, replacing the default sub-field stack.
16
16
  @binding {object} row The current row's data object.
17
- @binding {number} index The row's zero-based index.
17
+ @binding {number} index The row's zero-based position among visible rows.
18
18
  @binding {FieldDefinition[]} fields The sub-field definitions for the row.
19
- @binding {function} remove Removes this row (respects `minItems`).
19
+ @binding {function} remove Removes this row (respects `minItems`; soft-deletes instead of splicing when `field.softDeleteKey` is set and the row has an `id`).
20
20
  @binding {string} path The dot path into `form.data` for this row (e.g. `lines.0`).
21
21
  -->
22
22
  <slot
23
23
  v-if="$slots.row"
24
24
  name="row"
25
- :row="row"
26
- :index="index"
25
+ :row="entry.row"
26
+ :index="position"
27
27
  :fields="subFields"
28
- :remove="() => removeRow(index)"
29
- :path="rowPath(index)"
28
+ :remove="() => removeRow(entry.index)"
29
+ :path="rowPath(entry.index)"
30
30
  />
31
31
 
32
32
  <!-- Default row: stack each sub-field -->
33
33
  <template v-else>
34
34
  <div class="dx-repeater-row-header">
35
- <span class="dx-repeater-row-index">{{ index + 1 }}</span>
35
+ <span class="dx-repeater-row-index">{{ position + 1 }}</span>
36
36
  <DButton
37
37
  variant="outline-danger"
38
38
  size="sm"
39
- :disabled="rows.length <= minItems"
40
- @click="removeRow(index)"
39
+ :disabled="visibleRows.length <= minItems"
40
+ @click="removeRow(entry.index)"
41
41
  >
42
42
  Remove
43
43
  </DButton>
@@ -47,9 +47,9 @@
47
47
  :key="subField.key"
48
48
  :field="subField"
49
49
  :form="form"
50
- :model="row"
51
- :key-path="`${rowPath(index)}.${subField.key}`"
52
- :error-key="`${rowPath(index)}.${subField.key}`"
50
+ :model="entry.row"
51
+ :key-path="`${rowPath(entry.index)}.${subField.key}`"
52
+ :error-key="`${rowPath(entry.index)}.${subField.key}`"
53
53
  />
54
54
  </template>
55
55
  </div>
@@ -57,7 +57,7 @@
57
57
  <DButton
58
58
  variant="outline-primary"
59
59
  size="sm"
60
- :disabled="maxItems !== undefined && rows.length >= maxItems"
60
+ :disabled="maxItems !== undefined && visibleRows.length >= maxItems"
61
61
  @click="addRow"
62
62
  >
63
63
  {{ field.addLabel || "Add" }}
@@ -97,10 +97,21 @@ const arrayPath = computed(() => props.keyPath ?? props.field.key);
97
97
  const subFields = computed<FieldDefinition[]>(() => props.field.fields ?? []);
98
98
  const minItems = computed(() => props.field.minItems ?? 0);
99
99
  const maxItems = computed(() => props.field.maxItems);
100
+ const softDeleteKey = computed(() => props.field.softDeleteKey);
101
+
102
+ function isSoftDeleted(row: any): boolean {
103
+ return Boolean(
104
+ softDeleteKey.value &&
105
+ row &&
106
+ typeof row === "object" &&
107
+ row[softDeleteKey.value],
108
+ );
109
+ }
100
110
 
101
111
  /**
102
- * The live rows array. Ensures form.data holds an array at the path so
103
- * push/splice are reactive even when the form was seeded without one.
112
+ * The full rows array, including any row flagged via `softDeleteKey`. Ensures
113
+ * form.data holds an array at the path so push/splice are reactive even when
114
+ * the form was seeded without one.
104
115
  */
105
116
  const rows = computed<any[]>(() => {
106
117
  const existing = getByPath(props.form.data, arrayPath.value);
@@ -108,6 +119,18 @@ const rows = computed<any[]>(() => {
108
119
  return [];
109
120
  });
110
121
 
122
+ /**
123
+ * Rows shown in the UI, paired with their real (unfiltered) array index —
124
+ * a soft-deleted row stays in `form.data` (so it's submitted) but is hidden
125
+ * here, and every path/splice operation must still target its true index,
126
+ * not its position among visible rows.
127
+ */
128
+ const visibleRows = computed<Array<{ row: any; index: number }>>(() =>
129
+ rows.value
130
+ .map((row, index) => ({ row, index }))
131
+ .filter(({ row }) => !isSoftDeleted(row)),
132
+ );
133
+
111
134
  const rowPath = (index: number): string => `${arrayPath.value}.${index}`;
112
135
 
113
136
  // Stable v-for keys tied to row object identity, so removing a middle row
@@ -118,8 +141,7 @@ const rowPath = (index: number): string => `${arrayPath.value}.${index}`;
118
141
  const generatedKeys = new WeakMap<object, number>();
119
142
  let nextGeneratedKey = 0;
120
143
 
121
- const rowKey = (index: number): string | number => {
122
- const row = rows.value[index];
144
+ const rowKey = (row: any, fallbackPosition: number): string | number => {
123
145
  if (row && typeof row === "object") {
124
146
  if (row.id !== undefined && row.id !== null) return row.id;
125
147
  let key = generatedKeys.get(row);
@@ -130,7 +152,7 @@ const rowKey = (index: number): string | number => {
130
152
  }
131
153
  return `gen-${key}`;
132
154
  }
133
- return index;
155
+ return fallbackPosition;
134
156
  };
135
157
 
136
158
  /** Default value for a freshly added row's sub-field. */
@@ -188,7 +210,7 @@ function clearArrayErrors(): void {
188
210
  }
189
211
 
190
212
  function addRow(): void {
191
- if (maxItems.value !== undefined && rows.value.length >= maxItems.value) {
213
+ if (maxItems.value !== undefined && visibleRows.value.length >= maxItems.value) {
192
214
  return;
193
215
  }
194
216
  // Ensure the array exists on form.data before pushing (path may be
@@ -202,8 +224,21 @@ function addRow(): void {
202
224
  }
203
225
 
204
226
  function removeRow(index: number): void {
205
- if (rows.value.length <= minItems.value) return;
206
- getByPath(props.form.data, arrayPath.value).splice(index, 1);
227
+ if (visibleRows.value.length <= minItems.value) return;
228
+ const array = getByPath(props.form.data, arrayPath.value);
229
+ const row = array?.[index];
230
+
231
+ // A row the server already knows about (carries an `id`) can't just be
232
+ // spliced away under an upsert-children contract — the server never
233
+ // learns it was removed. Flag it instead so it's still submitted, and
234
+ // let it disappear from the UI via `visibleRows`.
235
+ if (softDeleteKey.value && row && typeof row === "object" && row.id !== undefined && row.id !== null) {
236
+ row[softDeleteKey.value] = true;
237
+ clearArrayErrors();
238
+ return;
239
+ }
240
+
241
+ array.splice(index, 1);
207
242
  clearArrayErrors();
208
243
  }
209
244
  </script>
@@ -190,6 +190,19 @@ export interface FieldDefinition {
190
190
  minItems?: number;
191
191
  maxItems?: number;
192
192
 
193
+ /**
194
+ * For `repeater` fields backed by an upsert-children API contract (rows
195
+ * carrying a persisted `id` are only deleted server-side when submitted
196
+ * flagged, e.g. Laravel's `{ id, to_delete: true }` pattern): the
197
+ * sub-field key to set `true` when a row is removed, instead of splicing
198
+ * it out of the array. Only applies to rows that already have an `id`
199
+ * (freshly added, never-persisted rows are still spliced normally since
200
+ * the server has never seen them). Flagged rows remain in `form.data`
201
+ * (so they're submitted) but are hidden from the UI and excluded from
202
+ * `minItems`/`maxItems` counts.
203
+ */
204
+ softDeleteKey?: string;
205
+
193
206
  /** Disable the field (static or computed from the model). */
194
207
  disabled?: MaybeFn<boolean>;
195
208