@omnitend/dashboard-for-laravel 0.11.0 → 0.14.0

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.
@@ -9,7 +9,7 @@
9
9
  <DContainer :fluid="fluid" :class="containerClass">
10
10
  <DRow class="justify-content-center">
11
11
  <DCol :md="columnSize">
12
- <DCard>
12
+ <DXTableShell :card="card">
13
13
  <template v-if="title || createUrl || $slots.header" #header>
14
14
  <!-- @slot Card header content; overrides the default title heading and the "New {item}" button. -->
15
15
  <slot name="header">
@@ -69,13 +69,15 @@
69
69
  @update:model-value="handleFilterChange(field.key, $event as string)"
70
70
  />
71
71
 
72
- <!-- Select Filter -->
73
- <DFormSelect
72
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
73
+ <DAutocomplete
74
74
  v-else-if="field.filter === 'select'"
75
75
  :model-value="effectiveFilters[field.key] || ''"
76
- :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
76
+ :options="getFieldFilterOptions(field)"
77
+ :placeholder="field.filterPlaceholder || `All ${field.label || field.key}`"
77
78
  size="sm"
78
- @update:model-value="handleFilterChange(field.key, $event as string)"
79
+ open-on-focus
80
+ @update:model-value="handleFilterChange(field.key, ($event ?? '') as string)"
79
81
  />
80
82
 
81
83
  <!-- Number Filter -->
@@ -165,13 +167,15 @@
165
167
  @update:model-value="handleFilterChange(field.key, $event as string)"
166
168
  />
167
169
 
168
- <!-- Select Filter -->
169
- <DFormSelect
170
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
171
+ <DAutocomplete
170
172
  v-else-if="field.filter === 'select'"
171
173
  :model-value="effectiveFilters[field.key] || ''"
172
- :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
174
+ :options="getFieldFilterOptions(field)"
175
+ :placeholder="field.filterPlaceholder || `All ${field.label || field.key}`"
173
176
  size="sm"
174
- @update:model-value="handleFilterChange(field.key, $event as string)"
177
+ open-on-focus
178
+ @update:model-value="handleFilterChange(field.key, ($event ?? '') as string)"
175
179
  />
176
180
 
177
181
  <!-- Number Filter -->
@@ -262,13 +266,15 @@
262
266
  @update:model-value="handleFilterChange(field.key, $event as string)"
263
267
  />
264
268
 
265
- <!-- Select Filter -->
266
- <DFormSelect
269
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
270
+ <DAutocomplete
267
271
  v-else-if="field.filter === 'select'"
268
272
  :model-value="effectiveFilters[field.key] || ''"
269
- :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
273
+ :options="getFieldFilterOptions(field)"
274
+ :placeholder="field.filterPlaceholder || `All ${field.label || field.key}`"
270
275
  size="sm"
271
- @update:model-value="handleFilterChange(field.key, $event as string)"
276
+ open-on-focus
277
+ @update:model-value="handleFilterChange(field.key, ($event ?? '') as string)"
272
278
  />
273
279
 
274
280
  <!-- Number Filter -->
@@ -346,7 +352,7 @@
346
352
  <div v-else></div>
347
353
 
348
354
  <!-- Per-page selector -->
349
- <div v-if="clientSidePagination.total >= Math.min(...perPageOptions)" class="d-flex align-items-center gap-2">
355
+ <div v-if="shouldShowPerPageSelector" class="d-flex align-items-center gap-2">
350
356
  <label for="perPageSelectClientSide" class="mb-0 small text-muted">Per page</label>
351
357
  <DFormSelect
352
358
  id="perPageSelectClientSide"
@@ -473,7 +479,7 @@
473
479
  </div>
474
480
  </div>
475
481
  </div>
476
- </DCard>
482
+ </DXTableShell>
477
483
  </DCol>
478
484
  </DRow>
479
485
 
@@ -484,6 +490,15 @@
484
490
  :title="computedModalTitle"
485
491
  :size="editModalSize"
486
492
  >
493
+ <!-- Loading the full record for edit (showUrl fetch) -->
494
+ <div
495
+ v-if="editLoading"
496
+ class="dx-edit-loading d-flex align-items-center gap-2 text-muted mb-3"
497
+ >
498
+ <DSpinner small />
499
+ <span>Loading…</span>
500
+ </div>
501
+
487
502
  <!-- Edit/create form (tabbed when editTabs provided, flat otherwise) -->
488
503
  <DXForm
489
504
  v-if="editForm"
@@ -584,7 +599,7 @@
584
599
  <DButton
585
600
  v-if="deleteUrl && !isCreateMode"
586
601
  variant="danger"
587
- :disabled="editForm?.processing"
602
+ :disabled="editForm?.processing || editLoading"
588
603
  @click="handleDelete"
589
604
  >
590
605
  {{ pendingAction === 'delete' ? 'Deleting...' : 'Delete' }}
@@ -596,7 +611,7 @@
596
611
  </DButton>
597
612
  <DButton
598
613
  variant="primary"
599
- :disabled="editForm?.processing"
614
+ :disabled="editForm?.processing || editLoading"
600
615
  @click="handleEditSave"
601
616
  >
602
617
  <template v-if="isCreateMode">
@@ -619,15 +634,17 @@ import { router } from "@inertiajs/vue3";
619
634
  import axios from "axios";
620
635
  import pluralize from "pluralize";
621
636
  import { useToast } from "../../composables/useToast";
637
+ import { useForm } from "../../composables/useForm";
622
638
  import DContainer from "../base/DContainer.vue";
623
639
  import DRow from "../base/DRow.vue";
624
640
  import DCol from "../base/DCol.vue";
625
- import DCard from "../base/DCard.vue";
641
+ import DXTableShell from "./DXTableShell.vue";
626
642
  import DSpinner from "../base/DSpinner.vue";
627
643
  import DTable from "../base/DTable.vue";
628
644
  import DPagination from "../base/DPagination.vue";
629
645
  import DFormInput from "../base/DFormInput.vue";
630
646
  import DFormSelect from "../base/DFormSelect.vue";
647
+ import DAutocomplete from "../base/DAutocomplete.vue";
631
648
  import DModal from "../base/DModal.vue";
632
649
  import DButton from "../base/DButton.vue";
633
650
  import DXForm from "./DXForm.vue";
@@ -757,7 +774,8 @@ export interface Props<TItem = any> {
757
774
  /** Items per page (for provider mode, v-model support) */
758
775
  perPage?: number;
759
776
 
760
- /** Striped rows */
777
+ /** Striped (banded) rows. Off by default — banding adds little on short
778
+ * tables and can read as noise; opt in with `:striped="true"`. */
761
779
  striped?: boolean;
762
780
 
763
781
  /** Hover effect on rows */
@@ -775,6 +793,14 @@ export interface Props<TItem = any> {
775
793
  /** Column size (Bootstrap grid) */
776
794
  columnSize?: string | number;
777
795
 
796
+ /**
797
+ * Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,
798
+ * borderless variant — the same header, filter row, table and pagination
799
+ * rendered directly on the page background (no card border/shadow), for
800
+ * data-heavy admin index pages that read as busy inside a card.
801
+ */
802
+ card?: boolean;
803
+
778
804
  // Edit Modal Props
779
805
  /** Form field definitions for edit modal (if provided, enables edit on row click) */
780
806
  editFields?: any[]; // FieldDefinition[] - using any to avoid circular import
@@ -791,6 +817,16 @@ export interface Props<TItem = any> {
791
817
  /** API endpoint pattern for updates (e.g., "/api/products/:id") */
792
818
  editUrl?: string;
793
819
 
820
+ /**
821
+ * API endpoint pattern to fetch the FULL record for the edit modal
822
+ * (e.g., "/api/products/:id"). When set, opening a row seeds the form from
823
+ * this fetch (shown with a loading state) instead of the thin list row —
824
+ * use when the list payload omits fields the edit form needs (notes, nested
825
+ * relations, repeater rows, …). The response is unwrapped from `data.data`
826
+ * (Laravel API Resource) or used as-is.
827
+ */
828
+ showUrl?: string;
829
+
794
830
  /** API endpoint pattern for deletions (e.g., "/api/products/:id") */
795
831
  deleteUrl?: string;
796
832
 
@@ -829,12 +865,13 @@ const props = withDefaults(defineProps<Props<T>>(), {
829
865
  perPageOptions: () => [10, 25, 50, 100],
830
866
  currentPage: 1,
831
867
  // perPage: 10, // Don't set default - let internalPerPage handle it
832
- striped: true,
868
+ striped: false,
833
869
  hover: true,
834
870
  responsive: true,
835
871
  fluid: false,
836
872
  containerClass: "py-5",
837
873
  columnSize: "12",
874
+ card: true,
838
875
  editModalSize: "lg",
839
876
  });
840
877
 
@@ -1185,9 +1222,11 @@ const shouldShowPerPageSelector = computed(() => {
1185
1222
  if (!props.showPerPageSelector) return false;
1186
1223
 
1187
1224
  const smallestOption = Math.min(...props.perPageOptions);
1188
- const total = isInertiaMode.value
1189
- ? props.pagination?.total || 0
1190
- : apiPaginationMeta.value?.total || 0;
1225
+ const total = isClientSideMode.value
1226
+ ? clientSidePagination.value?.total || 0
1227
+ : isInertiaMode.value
1228
+ ? props.pagination?.total || 0
1229
+ : apiPaginationMeta.value?.total || 0;
1191
1230
 
1192
1231
  return total >= smallestOption;
1193
1232
  });
@@ -1472,6 +1511,13 @@ const isCreateMode = ref(false);
1472
1511
  // request the form makes, so it can't tell Save from Delete on its own.
1473
1512
  const pendingAction = ref<'save' | 'delete' | null>(null);
1474
1513
 
1514
+ // True while the edit modal is fetching the full record via `showUrl`.
1515
+ const editLoading = ref(false);
1516
+
1517
+ // Monotonic token so a slow fetch for a previously-opened row can't overwrite
1518
+ // the form after the user has since opened a different row.
1519
+ let editFetchToken = 0;
1520
+
1475
1521
  // Toast (may not be available in test environment)
1476
1522
  let createToast: ((obj: any) => any) | undefined;
1477
1523
  try {
@@ -1552,29 +1598,68 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
1552
1598
  // Reset to first tab
1553
1599
  activeTabIndex.value = 0;
1554
1600
 
1555
- // Initialize form with item data
1601
+ // Initialize form with item data. `useForm` is statically imported, so
1602
+ // seeding is synchronous — no interleaving between successive row opens.
1556
1603
  if (!editForm.value) {
1557
- // Dynamically import useForm to avoid circular dependency
1558
- import('../../composables/useForm').then(({ useForm }) => {
1559
- const formData: Record<string, any> = {};
1560
- props.editFields!.forEach(field => {
1561
- formData[field.key] = (item as any)[field.key] ?? field.default ?? '';
1562
- });
1563
- editForm.value = useForm(formData);
1564
-
1565
- // Open modal
1566
- showEditModal.value = true;
1604
+ const formData: Record<string, any> = {};
1605
+ props.editFields!.forEach(field => {
1606
+ formData[field.key] = (item as any)[field.key] ?? field.default ?? '';
1567
1607
  });
1608
+ editForm.value = useForm(formData);
1568
1609
  } else {
1569
1610
  // Update existing form
1570
1611
  props.editFields.forEach(field => {
1571
1612
  editForm.value.data[field.key] = (item as any)[field.key] ?? field.default ?? '';
1572
1613
  });
1573
1614
  editForm.value.clearErrors();
1615
+ }
1616
+
1617
+ // Open modal
1618
+ showEditModal.value = true;
1619
+
1620
+ // Optionally replace the row-seeded data with the full record.
1621
+ if (props.showUrl) void fetchFullRecordForEdit(item);
1622
+ }
1623
+ };
1624
+
1625
+ // Fetch the full record for the edit modal and re-seed the form from it. The
1626
+ // form is already showing row data; this fills in fields the list row omitted.
1627
+ const fetchFullRecordForEdit = async (item: T) => {
1628
+ const itemId = (item as any).id;
1629
+ if (itemId === undefined || itemId === null) return;
1574
1630
 
1575
- // Open modal
1576
- showEditModal.value = true;
1631
+ const token = ++editFetchToken;
1632
+ editLoading.value = true;
1633
+ try {
1634
+ const url = props.showUrl!.replace(':id', String(itemId));
1635
+ const response = await axios.get(url);
1636
+ // Superseded by a newer row-open — discard.
1637
+ if (token !== editFetchToken) return;
1638
+
1639
+ const record = response.data?.data ?? response.data;
1640
+ if (record && editForm.value) {
1641
+ props.editFields!.forEach(field => {
1642
+ if (Object.prototype.hasOwnProperty.call(record, field.key)) {
1643
+ editForm.value.data[field.key] = record[field.key];
1644
+ }
1645
+ });
1646
+ // Widen selectedItem so predicates / the delete guard see full data.
1647
+ selectedItem.value = { ...(selectedItem.value as any), ...record };
1577
1648
  }
1649
+ } catch (error: any) {
1650
+ if (token !== editFetchToken) return;
1651
+ const message =
1652
+ error?.response?.data?.message ??
1653
+ error?.message ??
1654
+ 'Failed to load the full record.';
1655
+ createToast?.({
1656
+ title: 'Error',
1657
+ body: message,
1658
+ variant: 'danger',
1659
+ modelValue: 5000,
1660
+ });
1661
+ } finally {
1662
+ if (token === editFetchToken) editLoading.value = false;
1578
1663
  }
1579
1664
  };
1580
1665
 
@@ -1586,27 +1671,25 @@ const handleCreateNew = () => {
1586
1671
  selectedItem.value = null;
1587
1672
  activeTabIndex.value = 0;
1588
1673
 
1589
- const initForm = (useForm: any) => {
1674
+ // Supersede any in-flight row-open fetch so a slow showUrl fetch from an
1675
+ // earlier edit can't seed over this create.
1676
+ editFetchToken++;
1677
+ editLoading.value = false;
1678
+
1679
+ if (!editForm.value) {
1590
1680
  const formData: Record<string, any> = {};
1591
1681
  props.editFields!.forEach(field => {
1592
1682
  formData[field.key] = field.default ?? '';
1593
1683
  });
1594
1684
  editForm.value = useForm(formData);
1595
- showEditModal.value = true;
1596
- };
1597
-
1598
- if (!editForm.value) {
1599
- import('../../composables/useForm').then(({ useForm }) => {
1600
- initForm(useForm);
1601
- });
1602
1685
  } else {
1603
1686
  // Reset existing form to defaults
1604
1687
  props.editFields!.forEach(field => {
1605
1688
  editForm.value.data[field.key] = field.default ?? '';
1606
1689
  });
1607
1690
  editForm.value.clearErrors();
1608
- showEditModal.value = true;
1609
1691
  }
1692
+ showEditModal.value = true;
1610
1693
  };
1611
1694
 
1612
1695
  // Handle save from edit modal
@@ -1734,6 +1817,9 @@ const handleEditCancel = () => {
1734
1817
  selectedItem.value = null;
1735
1818
  isCreateMode.value = false;
1736
1819
  activeTabIndex.value = 0; // Reset tab for next time
1820
+ // Abandon any in-flight showUrl fetch so it can't seed a closed modal.
1821
+ editFetchToken++;
1822
+ editLoading.value = false;
1737
1823
  if (editForm.value) {
1738
1824
  editForm.value.clearErrors();
1739
1825
  }
@@ -1742,6 +1828,9 @@ const handleEditCancel = () => {
1742
1828
  // Handle delete from edit modal
1743
1829
  const handleDelete = async () => {
1744
1830
  if (!editForm.value || !selectedItem.value || !props.deleteUrl) return;
1831
+ // Don't evaluate the guard until the full record has loaded (showUrl) — it
1832
+ // may depend on fields the thin list row doesn't carry.
1833
+ if (editLoading.value) return;
1745
1834
 
1746
1835
  // Delete guard: a non-null message means this item can't be deleted — show
1747
1836
  // it immediately and skip the confirm and the request entirely.
@@ -0,0 +1,54 @@
1
+ <!--
2
+ @component
3
+ Internal chrome wrapper for `DXTable`. Renders its content either inside a
4
+ `DCard` (the default) or as a plain, borderless block on the page background
5
+ (`:card="false"`). Extracted so `DXTable`'s ~460-line body and all its slots
6
+ aren't duplicated across a card/plain `v-if`.
7
+
8
+ Slots:
9
+ - `#header` — the title / action row. In card mode it fills `DCard`'s header;
10
+ in plain mode it renders above the table with a subtle bottom rule.
11
+ - default — the table body (spinner/error/table/pagination).
12
+ -->
13
+ <template>
14
+ <DCard v-if="card">
15
+ <template v-if="$slots.header" #header>
16
+ <slot name="header" />
17
+ </template>
18
+ <slot />
19
+ </DCard>
20
+
21
+ <div v-else class="dx-table-plain">
22
+ <div v-if="$slots.header" class="dx-table-plain__header">
23
+ <slot name="header" />
24
+ </div>
25
+ <slot />
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import DCard from '../base/DCard.vue';
31
+
32
+ interface Props {
33
+ /** Wrap the content in a `DCard`. When false, render plain/borderless. */
34
+ card?: boolean;
35
+ }
36
+
37
+ withDefaults(defineProps<Props>(), {
38
+ card: true,
39
+ });
40
+ </script>
41
+
42
+ <style scoped>
43
+ /*
44
+ Plain variant: no card border/shadow/background — the table sits directly on
45
+ the page. The header row gets a subtle bottom rule so it still reads as a
46
+ distinct band above the table (mirrors the card header's separation without
47
+ the full card chrome).
48
+ */
49
+ .dx-table-plain__header {
50
+ padding-bottom: 0.75rem;
51
+ margin-bottom: 0.75rem;
52
+ border-bottom: 1px solid var(--bs-border-color);
53
+ }
54
+ </style>
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  reactive,
3
3
  computed,
4
+ toRaw,
4
5
  type ComputedRef,
5
6
  type WritableComputedRef,
6
7
  } from "vue";
@@ -71,10 +72,69 @@ export interface UseFormReturn<TData extends Record<string, any>>
71
72
 
72
73
  // ————————————————— helpers
73
74
 
74
- const deepClone = <T>(v: T): T =>
75
- typeof structuredClone === "function"
76
- ? structuredClone(v)
77
- : JSON.parse(JSON.stringify(v));
75
+ // Deep-clone the seed data for the form's working copy. `structuredClone`
76
+ // refuses to clone a Vue reactive Proxy (a `default: []` / `default: {}` inside
77
+ // a reactive `editFields` ref is a Proxy) and throws DataCloneError. `toRaw`
78
+ // unwraps the top-level proxy; a try/catch falls back to a JSON clone for any
79
+ // residual proxy (nested reactive values) or otherwise-uncloneable input. Form
80
+ // data is JSON-bound at the API boundary, so the JSON fallback is lossless here.
81
+ const deepClone = <T>(value: T): T => {
82
+ const raw = toRaw(value);
83
+ if (typeof structuredClone === "function") {
84
+ try {
85
+ return structuredClone(raw);
86
+ } catch {
87
+ // Fall through to the JSON clone below.
88
+ }
89
+ }
90
+ return JSON.parse(JSON.stringify(raw));
91
+ };
92
+
93
+ const isFileLike = (value: unknown): boolean =>
94
+ (typeof File !== "undefined" && value instanceof File) ||
95
+ (typeof Blob !== "undefined" && value instanceof Blob);
96
+
97
+ /** Whether a payload contains a File/Blob anywhere (→ needs multipart). */
98
+ function containsFile(value: unknown): boolean {
99
+ if (isFileLike(value)) return true;
100
+ if (Array.isArray(value)) return value.some(containsFile);
101
+ if (value !== null && typeof value === "object") {
102
+ return Object.values(value as Record<string, unknown>).some(containsFile);
103
+ }
104
+ return false;
105
+ }
106
+
107
+ /** Append a value to FormData using Laravel-style bracket keys. */
108
+ function appendToFormData(fd: FormData, key: string, value: unknown): void {
109
+ if (value === null || value === undefined) {
110
+ fd.append(key, "");
111
+ } else if (isFileLike(value)) {
112
+ fd.append(key, value as Blob);
113
+ } else if (Array.isArray(value)) {
114
+ value.forEach((entry, index) =>
115
+ appendToFormData(fd, `${key}[${index}]`, entry),
116
+ );
117
+ } else if (value instanceof Date) {
118
+ fd.append(key, value.toISOString());
119
+ } else if (typeof value === "object") {
120
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
121
+ appendToFormData(fd, `${key}[${k}]`, v);
122
+ }
123
+ } else if (typeof value === "boolean") {
124
+ // Laravel's boolean validation accepts "1"/"0".
125
+ fd.append(key, value ? "1" : "0");
126
+ } else {
127
+ fd.append(key, String(value));
128
+ }
129
+ }
130
+
131
+ function objectToFormData(payload: Record<string, unknown>): FormData {
132
+ const fd = new FormData();
133
+ for (const [key, value] of Object.entries(payload)) {
134
+ appendToFormData(fd, key, value);
135
+ }
136
+ return fd;
137
+ }
78
138
 
79
139
  function errorsFromLaravel(error: any): {
80
140
  errors: ValidationErrors;
@@ -201,15 +261,39 @@ export function useForm<TData extends Record<string, any>>(
201
261
 
202
262
  options.onBefore?.(state.data as TData);
203
263
 
264
+ // When the payload carries a File/Blob (an image/file field), send it as
265
+ // multipart/form-data. PHP only parses multipart bodies on POST, so a
266
+ // put/patch is spoofed as POST + `_method` (Laravel reads that). This
267
+ // also covers a `transform` that returns a FormData directly.
268
+ let sendMethod = method;
269
+ let sendPayload: any = payloadRaw;
270
+ if (method !== "get" && payloadRaw !== null && typeof payloadRaw === "object") {
271
+ const alreadyFormData =
272
+ typeof FormData !== "undefined" && payloadRaw instanceof FormData;
273
+ let formData: FormData | null = null;
274
+ if (alreadyFormData) {
275
+ formData = payloadRaw as FormData;
276
+ } else if (containsFile(payloadRaw)) {
277
+ formData = objectToFormData(payloadRaw as Record<string, unknown>);
278
+ }
279
+ if (formData) {
280
+ if (method === "put" || method === "patch") {
281
+ formData.append("_method", method.toUpperCase());
282
+ sendMethod = "post";
283
+ }
284
+ sendPayload = formData;
285
+ }
286
+ }
287
+
204
288
  try {
205
289
  const { data } =
206
- method === "get"
290
+ sendMethod === "get"
207
291
  ? await api.get<TResponse>(
208
292
  url,
209
- payloadRaw as Record<string, unknown>,
293
+ sendPayload as Record<string, unknown>,
210
294
  { signal: options?.signal },
211
295
  )
212
- : await api[method]<TResponse>(url, payloadRaw, {
296
+ : await api[sendMethod]<TResponse>(url, sendPayload, {
213
297
  signal: options?.signal,
214
298
  });
215
299
 
@@ -9,6 +9,10 @@ export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
9
9
  export { default as DXForm } from "./components/extended/DXForm.vue";
10
10
  export { default as DXField } from "./components/extended/DXField.vue";
11
11
  export { default as DXFieldLabel } from "./components/extended/DXFieldLabel.vue";
12
+ export { default as DXStatCard } from "./components/extended/DXStatCard.vue";
13
+ export { default as DXBarChart } from "./components/charts/DXBarChart.vue";
14
+ export { default as DXLineChart } from "./components/charts/DXLineChart.vue";
15
+ export { default as DXDoughnutChart } from "./components/charts/DXDoughnutChart.vue";
12
16
  export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
13
17
  /**
14
18
  * @deprecated Use `DXForm`. `DXBasicForm` is a thin wrapper around `DXForm`