@omnitend/dashboard-for-laravel 0.10.0 → 0.13.1
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.
- package/dist/components/charts/DXBarChart.vue.d.ts +22 -0
- package/dist/components/charts/DXDoughnutChart.vue.d.ts +24 -0
- package/dist/components/charts/DXLineChart.vue.d.ts +22 -0
- package/dist/components/charts/chartTheme.d.ts +32 -0
- package/dist/components/extended/DXDashboard.vue.d.ts +12 -2
- package/dist/components/extended/DXStatCard.vue.d.ts +56 -0
- package/dist/components/extended/DXTable.vue.d.ts +26 -1
- package/dist/dashboard-for-laravel.js +7457 -7034
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/style.css +1 -1
- package/docs/public/api-reference.json +322 -7
- package/docs/public/docs-map.md +4 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +12 -2
- package/resources/js/components/charts/DXBarChart.vue +79 -0
- package/resources/js/components/charts/DXDoughnutChart.vue +85 -0
- package/resources/js/components/charts/DXLineChart.vue +79 -0
- package/resources/js/components/charts/chartTheme.ts +229 -0
- package/resources/js/components/extended/DXDashboard.vue +18 -3
- package/resources/js/components/extended/DXStatCard.vue +211 -0
- package/resources/js/components/extended/DXTable.vue +137 -8
- package/resources/js/composables/useForm.ts +91 -7
- package/resources/js/index.ts +4 -0
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
<div v-else></div>
|
|
347
347
|
|
|
348
348
|
<!-- Per-page selector -->
|
|
349
|
-
<div v-if="
|
|
349
|
+
<div v-if="shouldShowPerPageSelector" class="d-flex align-items-center gap-2">
|
|
350
350
|
<label for="perPageSelectClientSide" class="mb-0 small text-muted">Per page</label>
|
|
351
351
|
<DFormSelect
|
|
352
352
|
id="perPageSelectClientSide"
|
|
@@ -484,6 +484,15 @@
|
|
|
484
484
|
:title="computedModalTitle"
|
|
485
485
|
:size="editModalSize"
|
|
486
486
|
>
|
|
487
|
+
<!-- Loading the full record for edit (showUrl fetch) -->
|
|
488
|
+
<div
|
|
489
|
+
v-if="editLoading"
|
|
490
|
+
class="dx-edit-loading d-flex align-items-center gap-2 text-muted mb-3"
|
|
491
|
+
>
|
|
492
|
+
<DSpinner small />
|
|
493
|
+
<span>Loading…</span>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
487
496
|
<!-- Edit/create form (tabbed when editTabs provided, flat otherwise) -->
|
|
488
497
|
<DXForm
|
|
489
498
|
v-if="editForm"
|
|
@@ -584,7 +593,7 @@
|
|
|
584
593
|
<DButton
|
|
585
594
|
v-if="deleteUrl && !isCreateMode"
|
|
586
595
|
variant="danger"
|
|
587
|
-
:disabled="editForm?.processing"
|
|
596
|
+
:disabled="editForm?.processing || editLoading"
|
|
588
597
|
@click="handleDelete"
|
|
589
598
|
>
|
|
590
599
|
{{ pendingAction === 'delete' ? 'Deleting...' : 'Delete' }}
|
|
@@ -596,7 +605,7 @@
|
|
|
596
605
|
</DButton>
|
|
597
606
|
<DButton
|
|
598
607
|
variant="primary"
|
|
599
|
-
:disabled="editForm?.processing"
|
|
608
|
+
:disabled="editForm?.processing || editLoading"
|
|
600
609
|
@click="handleEditSave"
|
|
601
610
|
>
|
|
602
611
|
<template v-if="isCreateMode">
|
|
@@ -757,7 +766,8 @@ export interface Props<TItem = any> {
|
|
|
757
766
|
/** Items per page (for provider mode, v-model support) */
|
|
758
767
|
perPage?: number;
|
|
759
768
|
|
|
760
|
-
/** Striped rows
|
|
769
|
+
/** Striped (banded) rows. Off by default — banding adds little on short
|
|
770
|
+
* tables and can read as noise; opt in with `:striped="true"`. */
|
|
761
771
|
striped?: boolean;
|
|
762
772
|
|
|
763
773
|
/** Hover effect on rows */
|
|
@@ -791,9 +801,29 @@ export interface Props<TItem = any> {
|
|
|
791
801
|
/** API endpoint pattern for updates (e.g., "/api/products/:id") */
|
|
792
802
|
editUrl?: string;
|
|
793
803
|
|
|
804
|
+
/**
|
|
805
|
+
* API endpoint pattern to fetch the FULL record for the edit modal
|
|
806
|
+
* (e.g., "/api/products/:id"). When set, opening a row seeds the form from
|
|
807
|
+
* this fetch (shown with a loading state) instead of the thin list row —
|
|
808
|
+
* use when the list payload omits fields the edit form needs (notes, nested
|
|
809
|
+
* relations, repeater rows, …). The response is unwrapped from `data.data`
|
|
810
|
+
* (Laravel API Resource) or used as-is.
|
|
811
|
+
*/
|
|
812
|
+
showUrl?: string;
|
|
813
|
+
|
|
794
814
|
/** API endpoint pattern for deletions (e.g., "/api/products/:id") */
|
|
795
815
|
deleteUrl?: string;
|
|
796
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Guard run when Delete is clicked, before the confirm dialog and request.
|
|
819
|
+
* Return a message for a non-deletable item to show it immediately (as a
|
|
820
|
+
* toast) and skip both the confirm and the delete request; return
|
|
821
|
+
* `null`/`undefined` to proceed with the normal confirm + delete. Lets you
|
|
822
|
+
* short-circuit a doomed delete (e.g. a record with dependents that the
|
|
823
|
+
* server would reject) with an immediate, specific reason.
|
|
824
|
+
*/
|
|
825
|
+
deleteGuard?: (item: TItem) => string | null | undefined;
|
|
826
|
+
|
|
797
827
|
/** API endpoint for creating new items (e.g., "/api/products") — enables "New" button */
|
|
798
828
|
createUrl?: string;
|
|
799
829
|
|
|
@@ -819,7 +849,7 @@ const props = withDefaults(defineProps<Props<T>>(), {
|
|
|
819
849
|
perPageOptions: () => [10, 25, 50, 100],
|
|
820
850
|
currentPage: 1,
|
|
821
851
|
// perPage: 10, // Don't set default - let internalPerPage handle it
|
|
822
|
-
striped:
|
|
852
|
+
striped: false,
|
|
823
853
|
hover: true,
|
|
824
854
|
responsive: true,
|
|
825
855
|
fluid: false,
|
|
@@ -1175,9 +1205,11 @@ const shouldShowPerPageSelector = computed(() => {
|
|
|
1175
1205
|
if (!props.showPerPageSelector) return false;
|
|
1176
1206
|
|
|
1177
1207
|
const smallestOption = Math.min(...props.perPageOptions);
|
|
1178
|
-
const total =
|
|
1179
|
-
?
|
|
1180
|
-
:
|
|
1208
|
+
const total = isClientSideMode.value
|
|
1209
|
+
? clientSidePagination.value?.total || 0
|
|
1210
|
+
: isInertiaMode.value
|
|
1211
|
+
? props.pagination?.total || 0
|
|
1212
|
+
: apiPaginationMeta.value?.total || 0;
|
|
1181
1213
|
|
|
1182
1214
|
return total >= smallestOption;
|
|
1183
1215
|
});
|
|
@@ -1462,6 +1494,19 @@ const isCreateMode = ref(false);
|
|
|
1462
1494
|
// request the form makes, so it can't tell Save from Delete on its own.
|
|
1463
1495
|
const pendingAction = ref<'save' | 'delete' | null>(null);
|
|
1464
1496
|
|
|
1497
|
+
// True while the edit modal is fetching the full record via `showUrl`.
|
|
1498
|
+
const editLoading = ref(false);
|
|
1499
|
+
|
|
1500
|
+
// Monotonic token so a slow fetch for a previously-opened row can't overwrite
|
|
1501
|
+
// the form after the user has since opened a different row.
|
|
1502
|
+
let editFetchToken = 0;
|
|
1503
|
+
|
|
1504
|
+
// Monotonic token for opening the modal itself. The first edit dynamically
|
|
1505
|
+
// imports useForm; if the user clicks row A then row B before that import
|
|
1506
|
+
// resolves, A's callback must not seed/open/fetch over B. Each open captures
|
|
1507
|
+
// the current token and bails if it's since been superseded.
|
|
1508
|
+
let modalOpenToken = 0;
|
|
1509
|
+
|
|
1465
1510
|
// Toast (may not be available in test environment)
|
|
1466
1511
|
let createToast: ((obj: any) => any) | undefined;
|
|
1467
1512
|
try {
|
|
@@ -1542,10 +1587,15 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1542
1587
|
// Reset to first tab
|
|
1543
1588
|
activeTabIndex.value = 0;
|
|
1544
1589
|
|
|
1590
|
+
// Capture this open so a slow first-time useForm import can't seed the
|
|
1591
|
+
// form after the user has since opened a different row (or create).
|
|
1592
|
+
const openToken = ++modalOpenToken;
|
|
1593
|
+
|
|
1545
1594
|
// Initialize form with item data
|
|
1546
1595
|
if (!editForm.value) {
|
|
1547
1596
|
// Dynamically import useForm to avoid circular dependency
|
|
1548
1597
|
import('../../composables/useForm').then(({ useForm }) => {
|
|
1598
|
+
if (openToken !== modalOpenToken) return; // superseded
|
|
1549
1599
|
const formData: Record<string, any> = {};
|
|
1550
1600
|
props.editFields!.forEach(field => {
|
|
1551
1601
|
formData[field.key] = (item as any)[field.key] ?? field.default ?? '';
|
|
@@ -1554,6 +1604,9 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1554
1604
|
|
|
1555
1605
|
// Open modal
|
|
1556
1606
|
showEditModal.value = true;
|
|
1607
|
+
|
|
1608
|
+
// Optionally replace the row-seeded data with the full record.
|
|
1609
|
+
if (props.showUrl) void fetchFullRecordForEdit(item);
|
|
1557
1610
|
});
|
|
1558
1611
|
} else {
|
|
1559
1612
|
// Update existing form
|
|
@@ -1564,7 +1617,51 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1564
1617
|
|
|
1565
1618
|
// Open modal
|
|
1566
1619
|
showEditModal.value = true;
|
|
1620
|
+
|
|
1621
|
+
// Optionally replace the row-seeded data with the full record.
|
|
1622
|
+
if (props.showUrl) void fetchFullRecordForEdit(item);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
// Fetch the full record for the edit modal and re-seed the form from it. The
|
|
1628
|
+
// form is already showing row data; this fills in fields the list row omitted.
|
|
1629
|
+
const fetchFullRecordForEdit = async (item: T) => {
|
|
1630
|
+
const itemId = (item as any).id;
|
|
1631
|
+
if (itemId === undefined || itemId === null) return;
|
|
1632
|
+
|
|
1633
|
+
const token = ++editFetchToken;
|
|
1634
|
+
editLoading.value = true;
|
|
1635
|
+
try {
|
|
1636
|
+
const url = props.showUrl!.replace(':id', String(itemId));
|
|
1637
|
+
const response = await axios.get(url);
|
|
1638
|
+
// Superseded by a newer row-open — discard.
|
|
1639
|
+
if (token !== editFetchToken) return;
|
|
1640
|
+
|
|
1641
|
+
const record = response.data?.data ?? response.data;
|
|
1642
|
+
if (record && editForm.value) {
|
|
1643
|
+
props.editFields!.forEach(field => {
|
|
1644
|
+
if (Object.prototype.hasOwnProperty.call(record, field.key)) {
|
|
1645
|
+
editForm.value.data[field.key] = record[field.key];
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
// Widen selectedItem so predicates / the delete guard see full data.
|
|
1649
|
+
selectedItem.value = { ...(selectedItem.value as any), ...record };
|
|
1567
1650
|
}
|
|
1651
|
+
} catch (error: any) {
|
|
1652
|
+
if (token !== editFetchToken) return;
|
|
1653
|
+
const message =
|
|
1654
|
+
error?.response?.data?.message ??
|
|
1655
|
+
error?.message ??
|
|
1656
|
+
'Failed to load the full record.';
|
|
1657
|
+
createToast?.({
|
|
1658
|
+
title: 'Error',
|
|
1659
|
+
body: message,
|
|
1660
|
+
variant: 'danger',
|
|
1661
|
+
modelValue: 5000,
|
|
1662
|
+
});
|
|
1663
|
+
} finally {
|
|
1664
|
+
if (token === editFetchToken) editLoading.value = false;
|
|
1568
1665
|
}
|
|
1569
1666
|
};
|
|
1570
1667
|
|
|
@@ -1576,6 +1673,12 @@ const handleCreateNew = () => {
|
|
|
1576
1673
|
selectedItem.value = null;
|
|
1577
1674
|
activeTabIndex.value = 0;
|
|
1578
1675
|
|
|
1676
|
+
// Supersede any in-flight row-open (and its pending fetch) so a slow
|
|
1677
|
+
// useForm import from an earlier edit can't seed over this create.
|
|
1678
|
+
const openToken = ++modalOpenToken;
|
|
1679
|
+
editFetchToken++;
|
|
1680
|
+
editLoading.value = false;
|
|
1681
|
+
|
|
1579
1682
|
const initForm = (useForm: any) => {
|
|
1580
1683
|
const formData: Record<string, any> = {};
|
|
1581
1684
|
props.editFields!.forEach(field => {
|
|
@@ -1587,6 +1690,7 @@ const handleCreateNew = () => {
|
|
|
1587
1690
|
|
|
1588
1691
|
if (!editForm.value) {
|
|
1589
1692
|
import('../../composables/useForm').then(({ useForm }) => {
|
|
1693
|
+
if (openToken !== modalOpenToken) return; // superseded
|
|
1590
1694
|
initForm(useForm);
|
|
1591
1695
|
});
|
|
1592
1696
|
} else {
|
|
@@ -1724,6 +1828,9 @@ const handleEditCancel = () => {
|
|
|
1724
1828
|
selectedItem.value = null;
|
|
1725
1829
|
isCreateMode.value = false;
|
|
1726
1830
|
activeTabIndex.value = 0; // Reset tab for next time
|
|
1831
|
+
// Abandon any in-flight showUrl fetch so it can't seed a closed modal.
|
|
1832
|
+
editFetchToken++;
|
|
1833
|
+
editLoading.value = false;
|
|
1727
1834
|
if (editForm.value) {
|
|
1728
1835
|
editForm.value.clearErrors();
|
|
1729
1836
|
}
|
|
@@ -1732,6 +1839,22 @@ const handleEditCancel = () => {
|
|
|
1732
1839
|
// Handle delete from edit modal
|
|
1733
1840
|
const handleDelete = async () => {
|
|
1734
1841
|
if (!editForm.value || !selectedItem.value || !props.deleteUrl) return;
|
|
1842
|
+
// Don't evaluate the guard until the full record has loaded (showUrl) — it
|
|
1843
|
+
// may depend on fields the thin list row doesn't carry.
|
|
1844
|
+
if (editLoading.value) return;
|
|
1845
|
+
|
|
1846
|
+
// Delete guard: a non-null message means this item can't be deleted — show
|
|
1847
|
+
// it immediately and skip the confirm and the request entirely.
|
|
1848
|
+
const guardMessage = props.deleteGuard?.(selectedItem.value as T);
|
|
1849
|
+
if (guardMessage) {
|
|
1850
|
+
createToast?.({
|
|
1851
|
+
title: 'Cannot delete',
|
|
1852
|
+
body: guardMessage,
|
|
1853
|
+
variant: 'danger',
|
|
1854
|
+
modelValue: 5000,
|
|
1855
|
+
});
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1735
1858
|
|
|
1736
1859
|
// Confirm deletion
|
|
1737
1860
|
const itemName = (selectedItem.value as any).name || (selectedItem.value as any).title || singularItemName.value;
|
|
@@ -1786,6 +1909,12 @@ const handleDelete = async () => {
|
|
|
1786
1909
|
|
|
1787
1910
|
defineExpose({
|
|
1788
1911
|
refresh,
|
|
1912
|
+
/**
|
|
1913
|
+
* Open the built-in create modal (same as clicking the default "New {item}"
|
|
1914
|
+
* button). Lets the create action live outside the table card — e.g. in a
|
|
1915
|
+
* page header or the dashboard navbar. No-op unless `editFields` are set.
|
|
1916
|
+
*/
|
|
1917
|
+
openCreate: handleCreateNew,
|
|
1789
1918
|
});
|
|
1790
1919
|
</script>
|
|
1791
1920
|
|
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
290
|
+
sendMethod === "get"
|
|
207
291
|
? await api.get<TResponse>(
|
|
208
292
|
url,
|
|
209
|
-
|
|
293
|
+
sendPayload as Record<string, unknown>,
|
|
210
294
|
{ signal: options?.signal },
|
|
211
295
|
)
|
|
212
|
-
: await api[
|
|
296
|
+
: await api[sendMethod]<TResponse>(url, sendPayload, {
|
|
213
297
|
signal: options?.signal,
|
|
214
298
|
});
|
|
215
299
|
|
package/resources/js/index.ts
CHANGED
|
@@ -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`
|