@omnitend/dashboard-for-laravel 0.4.13 → 0.4.14
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/extended/DXTable.vue.d.ts +6 -2
- package/dist/dashboard-for-laravel.js +5084 -5014
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +5 -5
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +6 -0
- package/package.json +1 -1
- package/resources/js/components/extended/DXBasicForm.vue +14 -2
- package/resources/js/components/extended/DXTable.vue +116 -4
- package/resources/js/types/index.ts +7 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -34,4 +34,10 @@ export interface FieldDefinition {
|
|
|
34
34
|
class?: string;
|
|
35
35
|
/** Additional props to pass to the input component */
|
|
36
36
|
inputProps?: Record<string, any>;
|
|
37
|
+
/**
|
|
38
|
+
* Conditionally show or hide this field. When omitted, the field
|
|
39
|
+
* is always visible. The function is re-evaluated reactively, so
|
|
40
|
+
* it can depend on form state or other reactive sources.
|
|
41
|
+
*/
|
|
42
|
+
show?: () => boolean;
|
|
37
43
|
}
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</DAlert>
|
|
12
12
|
|
|
13
13
|
<!-- Render each field -->
|
|
14
|
-
<template v-for="field in
|
|
14
|
+
<template v-for="field in visibleFields" :key="field.key">
|
|
15
15
|
<!-- Custom slot for this field -->
|
|
16
16
|
<slot :name="`field-${field.key}`" :field="field" :form="form">
|
|
17
17
|
<!-- Default field rendering -->
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
</template>
|
|
112
112
|
|
|
113
113
|
<script setup lang="ts">
|
|
114
|
+
import { computed } from "vue";
|
|
114
115
|
import { BForm, BFormRadioGroup } from "bootstrap-vue-next";
|
|
115
116
|
import DAlert from "../base/DAlert.vue";
|
|
116
117
|
import DFormGroup from "../base/DFormGroup.vue";
|
|
@@ -141,12 +142,23 @@ interface Props {
|
|
|
141
142
|
showSubmit?: boolean;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
withDefaults(defineProps<Props>(), {
|
|
145
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
145
146
|
submitText: "Submit",
|
|
146
147
|
submitLoadingText: "Submitting...",
|
|
147
148
|
showSubmit: true,
|
|
148
149
|
});
|
|
149
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Fields whose `show()` predicate evaluates to true (or omits the
|
|
153
|
+
* predicate entirely). Computed so the form re-renders when reactive
|
|
154
|
+
* sources used inside `show` change.
|
|
155
|
+
*/
|
|
156
|
+
const visibleFields = computed(() => {
|
|
157
|
+
return props.fields.filter((field) =>
|
|
158
|
+
field.show ? field.show() : true,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
150
162
|
const emit = defineEmits<{
|
|
151
163
|
submit: [];
|
|
152
164
|
}>();
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
<DRow class="justify-content-center">
|
|
4
4
|
<DCol :md="columnSize">
|
|
5
5
|
<DCard>
|
|
6
|
-
<template v-if="title || $slots.header" #header>
|
|
6
|
+
<template v-if="title || createUrl || $slots.header" #header>
|
|
7
7
|
<slot name="header">
|
|
8
8
|
<div class="d-flex justify-content-between align-items-center">
|
|
9
9
|
<h4 class="mb-0">{{ title }}</h4>
|
|
10
|
+
<DButton
|
|
11
|
+
v-if="createUrl"
|
|
12
|
+
variant="primary"
|
|
13
|
+
size="sm"
|
|
14
|
+
@click="handleCreateNew"
|
|
15
|
+
>
|
|
16
|
+
New {{ singularItemName }}
|
|
17
|
+
</DButton>
|
|
10
18
|
</div>
|
|
11
19
|
</slot>
|
|
12
20
|
</template>
|
|
@@ -574,7 +582,7 @@
|
|
|
574
582
|
<div class="d-flex justify-content-between w-100">
|
|
575
583
|
<div>
|
|
576
584
|
<DButton
|
|
577
|
-
v-if="deleteUrl"
|
|
585
|
+
v-if="deleteUrl && !isCreateMode"
|
|
578
586
|
variant="danger"
|
|
579
587
|
:disabled="editForm?.processing"
|
|
580
588
|
@click="handleDelete"
|
|
@@ -591,7 +599,12 @@
|
|
|
591
599
|
:disabled="editForm?.processing"
|
|
592
600
|
@click="handleEditSave"
|
|
593
601
|
>
|
|
594
|
-
|
|
602
|
+
<template v-if="isCreateMode">
|
|
603
|
+
{{ editForm?.processing ? 'Creating...' : 'Create' }}
|
|
604
|
+
</template>
|
|
605
|
+
<template v-else>
|
|
606
|
+
{{ editForm?.processing ? 'Saving...' : 'Save Changes' }}
|
|
607
|
+
</template>
|
|
595
608
|
</DButton>
|
|
596
609
|
</div>
|
|
597
610
|
</div>
|
|
@@ -784,6 +797,9 @@ export interface Props<TItem = any> {
|
|
|
784
797
|
/** API endpoint pattern for deletions (e.g., "/api/products/:id") */
|
|
785
798
|
deleteUrl?: string;
|
|
786
799
|
|
|
800
|
+
/** API endpoint for creating new items (e.g., "/api/products") — enables "New" button */
|
|
801
|
+
createUrl?: string;
|
|
802
|
+
|
|
787
803
|
/** Enable client-side filtering, sorting, and pagination on items array */
|
|
788
804
|
clientSide?: boolean;
|
|
789
805
|
}
|
|
@@ -821,6 +837,8 @@ const emit = defineEmits<{
|
|
|
821
837
|
filterChange: [filters: Record<string, string>];
|
|
822
838
|
perPageChange: [perPage: number];
|
|
823
839
|
rowClicked: [item: T, index: number, event: MouseEvent];
|
|
840
|
+
rowCreated: [item: any, response: any];
|
|
841
|
+
createError: [error: any];
|
|
824
842
|
rowUpdated: [item: T, response: any];
|
|
825
843
|
editError: [item: T, error: any];
|
|
826
844
|
rowDeleted: [item: T, response: any];
|
|
@@ -1425,6 +1443,7 @@ const showEditModal = ref(false);
|
|
|
1425
1443
|
const selectedItem = ref<T | null>(null);
|
|
1426
1444
|
const editForm = ref<any>(null);
|
|
1427
1445
|
const activeTabIndex = ref(0);
|
|
1446
|
+
const isCreateMode = ref(false);
|
|
1428
1447
|
|
|
1429
1448
|
// Toast (may not be available in test environment)
|
|
1430
1449
|
let createToast: ((obj: any) => any) | undefined;
|
|
@@ -1504,6 +1523,9 @@ const pluralItemName = computed(() => pluralize(props.itemName));
|
|
|
1504
1523
|
|
|
1505
1524
|
// Computed: Modal title (supports function)
|
|
1506
1525
|
const computedModalTitle = computed(() => {
|
|
1526
|
+
if (isCreateMode.value) {
|
|
1527
|
+
return `New ${singularItemName.value}`;
|
|
1528
|
+
}
|
|
1507
1529
|
if (!selectedItem.value) {
|
|
1508
1530
|
return `Edit ${singularItemName.value}`;
|
|
1509
1531
|
}
|
|
@@ -1529,6 +1551,7 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1529
1551
|
// If editFields provided, open edit modal
|
|
1530
1552
|
if (props.editFields && props.editFields.length > 0) {
|
|
1531
1553
|
// Set selected item FIRST before any rendering
|
|
1554
|
+
isCreateMode.value = false;
|
|
1532
1555
|
selectedItem.value = item;
|
|
1533
1556
|
|
|
1534
1557
|
// Reset to first tab
|
|
@@ -1560,9 +1583,97 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1560
1583
|
}
|
|
1561
1584
|
};
|
|
1562
1585
|
|
|
1586
|
+
// Handle "New" button click
|
|
1587
|
+
const handleCreateNew = () => {
|
|
1588
|
+
if (!props.editFields || props.editFields.length === 0) return;
|
|
1589
|
+
|
|
1590
|
+
isCreateMode.value = true;
|
|
1591
|
+
selectedItem.value = null;
|
|
1592
|
+
activeTabIndex.value = 0;
|
|
1593
|
+
|
|
1594
|
+
const initForm = (useForm: any) => {
|
|
1595
|
+
const formData: Record<string, any> = {};
|
|
1596
|
+
props.editFields!.forEach(field => {
|
|
1597
|
+
formData[field.key] = field.default ?? '';
|
|
1598
|
+
});
|
|
1599
|
+
editForm.value = useForm(formData);
|
|
1600
|
+
showEditModal.value = true;
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
if (!editForm.value) {
|
|
1604
|
+
import('../../composables/useForm').then(({ useForm }) => {
|
|
1605
|
+
initForm(useForm);
|
|
1606
|
+
});
|
|
1607
|
+
} else {
|
|
1608
|
+
// Reset existing form to defaults
|
|
1609
|
+
props.editFields!.forEach(field => {
|
|
1610
|
+
editForm.value.data[field.key] = field.default ?? '';
|
|
1611
|
+
});
|
|
1612
|
+
editForm.value.clearErrors();
|
|
1613
|
+
showEditModal.value = true;
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1563
1617
|
// Handle save from edit modal
|
|
1564
1618
|
const handleEditSave = async () => {
|
|
1565
|
-
if (!editForm.value
|
|
1619
|
+
if (!editForm.value) return;
|
|
1620
|
+
|
|
1621
|
+
// Create mode: POST to createUrl
|
|
1622
|
+
if (isCreateMode.value && props.createUrl) {
|
|
1623
|
+
try {
|
|
1624
|
+
await editForm.value.post(props.createUrl, {
|
|
1625
|
+
onSuccess: (data: any) => {
|
|
1626
|
+
createToast?.({
|
|
1627
|
+
title: 'Success',
|
|
1628
|
+
body: `${singularItemName.value} created successfully`,
|
|
1629
|
+
variant: 'success',
|
|
1630
|
+
modelValue: 3000,
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
emit('rowCreated', data?.data ?? data, data);
|
|
1634
|
+
showEditModal.value = false;
|
|
1635
|
+
selectedItem.value = null;
|
|
1636
|
+
isCreateMode.value = false;
|
|
1637
|
+
|
|
1638
|
+
refresh();
|
|
1639
|
+
},
|
|
1640
|
+
onError: (errors: any) => {
|
|
1641
|
+
let errorMessage = 'Failed to create. Please check the form for errors.';
|
|
1642
|
+
if (errors && typeof errors === 'object') {
|
|
1643
|
+
const firstError = Object.values(errors).flat()[0];
|
|
1644
|
+
if (typeof firstError === 'string') {
|
|
1645
|
+
errorMessage = firstError;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
createToast?.({
|
|
1650
|
+
title: 'Error',
|
|
1651
|
+
body: errorMessage,
|
|
1652
|
+
variant: 'danger',
|
|
1653
|
+
modelValue: 5000,
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
if (props.editTabs && props.editTabs.length > 0) {
|
|
1657
|
+
const errorKeys = Object.keys(errors);
|
|
1658
|
+
const tabIndex = visibleTabs.value.findIndex(tab =>
|
|
1659
|
+
tab.fieldKeys.some(key => errorKeys.includes(key))
|
|
1660
|
+
);
|
|
1661
|
+
if (tabIndex !== -1) {
|
|
1662
|
+
activeTabIndex.value = tabIndex;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
emit('createError', errors);
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
emit('createError', error);
|
|
1671
|
+
}
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// Edit mode: PUT to editUrl
|
|
1676
|
+
if (!selectedItem.value) return;
|
|
1566
1677
|
|
|
1567
1678
|
try {
|
|
1568
1679
|
// If editUrl provided, handle API call internally
|
|
@@ -1634,6 +1745,7 @@ const handleEditSave = async () => {
|
|
|
1634
1745
|
const handleEditCancel = () => {
|
|
1635
1746
|
showEditModal.value = false;
|
|
1636
1747
|
selectedItem.value = null;
|
|
1748
|
+
isCreateMode.value = false;
|
|
1637
1749
|
activeTabIndex.value = 0; // Reset tab for next time
|
|
1638
1750
|
if (editForm.value) {
|
|
1639
1751
|
editForm.value.clearErrors();
|
|
@@ -58,4 +58,11 @@ export interface FieldDefinition {
|
|
|
58
58
|
|
|
59
59
|
/** Additional props to pass to the input component */
|
|
60
60
|
inputProps?: Record<string, any>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Conditionally show or hide this field. When omitted, the field
|
|
64
|
+
* is always visible. The function is re-evaluated reactively, so
|
|
65
|
+
* it can depend on form state or other reactive sources.
|
|
66
|
+
*/
|
|
67
|
+
show?: () => boolean;
|
|
61
68
|
}
|