@omnitend/dashboard-for-laravel 0.4.11 → 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 +6942 -6837
- 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 -2
- package/resources/js/components/extended/DXBasicForm.vue +14 -2
- package/resources/js/components/extended/DXTable.vue +180 -6
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnitend/dashboard-for-laravel",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
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",
|
|
@@ -90,7 +90,6 @@
|
|
|
90
90
|
"vite": "^6.4.1",
|
|
91
91
|
"vitest": "^4.0.3",
|
|
92
92
|
"vitest-browser-vue": "^2.0.0",
|
|
93
|
-
"vue": "^3.5.25",
|
|
94
93
|
"vue-tsc": "^2.2.12"
|
|
95
94
|
},
|
|
96
95
|
"keywords": [
|
|
@@ -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>
|
|
@@ -503,7 +511,7 @@
|
|
|
503
511
|
<!-- Other field types with label -->
|
|
504
512
|
<DFormGroup
|
|
505
513
|
v-else
|
|
506
|
-
:label="
|
|
514
|
+
:label="getFieldLabel(fieldKey)"
|
|
507
515
|
class="mb-3"
|
|
508
516
|
>
|
|
509
517
|
<!-- Custom value slot -->
|
|
@@ -521,20 +529,36 @@
|
|
|
521
529
|
:required="getField(fieldKey).required"
|
|
522
530
|
:rows="getField(fieldKey).rows || 3"
|
|
523
531
|
:state="editForm.getState(fieldKey)"
|
|
532
|
+
:disabled="isFieldDisabled(fieldKey)"
|
|
524
533
|
@input="editForm.clearError(fieldKey)"
|
|
525
534
|
/>
|
|
535
|
+
<DFormSelect
|
|
536
|
+
v-else-if="getField(fieldKey).type === 'select'"
|
|
537
|
+
v-model="editForm.data[fieldKey]"
|
|
538
|
+
:required="getField(fieldKey).required"
|
|
539
|
+
:options="getField(fieldKey).options"
|
|
540
|
+
:state="editForm.getState(fieldKey)"
|
|
541
|
+
:disabled="isFieldDisabled(fieldKey)"
|
|
542
|
+
@change="editForm.clearError(fieldKey)"
|
|
543
|
+
/>
|
|
526
544
|
<DFormInput
|
|
527
545
|
v-else
|
|
528
546
|
v-model="editForm.data[fieldKey]"
|
|
529
547
|
:type="getField(fieldKey).type || 'text'"
|
|
530
548
|
:required="getField(fieldKey).required"
|
|
549
|
+
:step="getField(fieldKey).step"
|
|
531
550
|
:state="editForm.getState(fieldKey)"
|
|
551
|
+
:disabled="isFieldDisabled(fieldKey)"
|
|
532
552
|
@input="editForm.clearError(fieldKey)"
|
|
533
553
|
/>
|
|
534
554
|
<!-- Validation error -->
|
|
535
555
|
<DFormInvalidFeedback v-if="editForm.hasError(fieldKey)">
|
|
536
556
|
{{ editForm.getError(fieldKey) }}
|
|
537
557
|
</DFormInvalidFeedback>
|
|
558
|
+
<!-- Hint text -->
|
|
559
|
+
<DFormText v-if="getFieldHint(fieldKey)" class="text-muted">
|
|
560
|
+
{{ getFieldHint(fieldKey) }}
|
|
561
|
+
</DFormText>
|
|
538
562
|
</DFormGroup>
|
|
539
563
|
</template>
|
|
540
564
|
|
|
@@ -549,7 +573,7 @@
|
|
|
549
573
|
<DXBasicForm
|
|
550
574
|
v-else-if="editForm"
|
|
551
575
|
:form="editForm"
|
|
552
|
-
:fields="
|
|
576
|
+
:fields="resolvedEditFields"
|
|
553
577
|
:show-submit="false"
|
|
554
578
|
@submit="handleEditSave"
|
|
555
579
|
/>
|
|
@@ -558,7 +582,7 @@
|
|
|
558
582
|
<div class="d-flex justify-content-between w-100">
|
|
559
583
|
<div>
|
|
560
584
|
<DButton
|
|
561
|
-
v-if="deleteUrl"
|
|
585
|
+
v-if="deleteUrl && !isCreateMode"
|
|
562
586
|
variant="danger"
|
|
563
587
|
:disabled="editForm?.processing"
|
|
564
588
|
@click="handleDelete"
|
|
@@ -575,7 +599,12 @@
|
|
|
575
599
|
:disabled="editForm?.processing"
|
|
576
600
|
@click="handleEditSave"
|
|
577
601
|
>
|
|
578
|
-
|
|
602
|
+
<template v-if="isCreateMode">
|
|
603
|
+
{{ editForm?.processing ? 'Creating...' : 'Create' }}
|
|
604
|
+
</template>
|
|
605
|
+
<template v-else>
|
|
606
|
+
{{ editForm?.processing ? 'Saving...' : 'Save Changes' }}
|
|
607
|
+
</template>
|
|
579
608
|
</DButton>
|
|
580
609
|
</div>
|
|
581
610
|
</div>
|
|
@@ -607,6 +636,7 @@ import DFormGroup from "../base/DFormGroup.vue";
|
|
|
607
636
|
import DFormTextarea from "../base/DFormTextarea.vue";
|
|
608
637
|
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
609
638
|
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
639
|
+
import DFormText from "../base/DFormText.vue";
|
|
610
640
|
import DXBasicForm from "./DXBasicForm.vue";
|
|
611
641
|
export type FilterType = 'text' | 'select' | 'number' | 'date' | false;
|
|
612
642
|
|
|
@@ -767,6 +797,9 @@ export interface Props<TItem = any> {
|
|
|
767
797
|
/** API endpoint pattern for deletions (e.g., "/api/products/:id") */
|
|
768
798
|
deleteUrl?: string;
|
|
769
799
|
|
|
800
|
+
/** API endpoint for creating new items (e.g., "/api/products") — enables "New" button */
|
|
801
|
+
createUrl?: string;
|
|
802
|
+
|
|
770
803
|
/** Enable client-side filtering, sorting, and pagination on items array */
|
|
771
804
|
clientSide?: boolean;
|
|
772
805
|
}
|
|
@@ -804,6 +837,8 @@ const emit = defineEmits<{
|
|
|
804
837
|
filterChange: [filters: Record<string, string>];
|
|
805
838
|
perPageChange: [perPage: number];
|
|
806
839
|
rowClicked: [item: T, index: number, event: MouseEvent];
|
|
840
|
+
rowCreated: [item: any, response: any];
|
|
841
|
+
createError: [error: any];
|
|
807
842
|
rowUpdated: [item: T, response: any];
|
|
808
843
|
editError: [item: T, error: any];
|
|
809
844
|
rowDeleted: [item: T, response: any];
|
|
@@ -1408,6 +1443,7 @@ const showEditModal = ref(false);
|
|
|
1408
1443
|
const selectedItem = ref<T | null>(null);
|
|
1409
1444
|
const editForm = ref<any>(null);
|
|
1410
1445
|
const activeTabIndex = ref(0);
|
|
1446
|
+
const isCreateMode = ref(false);
|
|
1411
1447
|
|
|
1412
1448
|
// Toast (may not be available in test environment)
|
|
1413
1449
|
let createToast: ((obj: any) => any) | undefined;
|
|
@@ -1436,12 +1472,60 @@ const getField = (key: string) => {
|
|
|
1436
1472
|
return props.editFields?.find(f => f.key === key) || { key };
|
|
1437
1473
|
};
|
|
1438
1474
|
|
|
1475
|
+
// Helper: Get field label (supports function for dynamic labels)
|
|
1476
|
+
const getFieldLabel = (key: string): string => {
|
|
1477
|
+
const field = getField(key);
|
|
1478
|
+
if (typeof field.label === 'function') {
|
|
1479
|
+
return field.label(selectedItem.value);
|
|
1480
|
+
}
|
|
1481
|
+
return field.label || key;
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
// Helper: Get field hint (supports function for dynamic hints)
|
|
1485
|
+
const getFieldHint = (key: string): string | undefined => {
|
|
1486
|
+
const field = getField(key);
|
|
1487
|
+
if (typeof field.hint === 'function') {
|
|
1488
|
+
return field.hint(selectedItem.value);
|
|
1489
|
+
}
|
|
1490
|
+
return field.hint;
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// Helper: Check if field is disabled (supports disabledWhen function)
|
|
1494
|
+
const isFieldDisabled = (key: string): boolean => {
|
|
1495
|
+
const field = getField(key);
|
|
1496
|
+
if (typeof field.disabledWhen === 'function') {
|
|
1497
|
+
return field.disabledWhen(selectedItem.value);
|
|
1498
|
+
}
|
|
1499
|
+
return field.disabled || false;
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
// Computed: Resolve edit fields with dynamic labels/hints for DXBasicForm
|
|
1503
|
+
const resolvedEditFields = computed(() => {
|
|
1504
|
+
if (!props.editFields) return [];
|
|
1505
|
+
|
|
1506
|
+
return props.editFields.map(field => ({
|
|
1507
|
+
...field,
|
|
1508
|
+
label: typeof field.label === 'function'
|
|
1509
|
+
? field.label(selectedItem.value)
|
|
1510
|
+
: field.label,
|
|
1511
|
+
hint: typeof field.hint === 'function'
|
|
1512
|
+
? field.hint(selectedItem.value)
|
|
1513
|
+
: field.hint,
|
|
1514
|
+
disabled: typeof field.disabledWhen === 'function'
|
|
1515
|
+
? field.disabledWhen(selectedItem.value)
|
|
1516
|
+
: field.disabled,
|
|
1517
|
+
}));
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1439
1520
|
// Computed: Singular and plural item names
|
|
1440
1521
|
const singularItemName = computed(() => props.itemName);
|
|
1441
1522
|
const pluralItemName = computed(() => pluralize(props.itemName));
|
|
1442
1523
|
|
|
1443
1524
|
// Computed: Modal title (supports function)
|
|
1444
1525
|
const computedModalTitle = computed(() => {
|
|
1526
|
+
if (isCreateMode.value) {
|
|
1527
|
+
return `New ${singularItemName.value}`;
|
|
1528
|
+
}
|
|
1445
1529
|
if (!selectedItem.value) {
|
|
1446
1530
|
return `Edit ${singularItemName.value}`;
|
|
1447
1531
|
}
|
|
@@ -1467,6 +1551,7 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1467
1551
|
// If editFields provided, open edit modal
|
|
1468
1552
|
if (props.editFields && props.editFields.length > 0) {
|
|
1469
1553
|
// Set selected item FIRST before any rendering
|
|
1554
|
+
isCreateMode.value = false;
|
|
1470
1555
|
selectedItem.value = item;
|
|
1471
1556
|
|
|
1472
1557
|
// Reset to first tab
|
|
@@ -1498,9 +1583,97 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1498
1583
|
}
|
|
1499
1584
|
};
|
|
1500
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
|
+
|
|
1501
1617
|
// Handle save from edit modal
|
|
1502
1618
|
const handleEditSave = async () => {
|
|
1503
|
-
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;
|
|
1504
1677
|
|
|
1505
1678
|
try {
|
|
1506
1679
|
// If editUrl provided, handle API call internally
|
|
@@ -1572,6 +1745,7 @@ const handleEditSave = async () => {
|
|
|
1572
1745
|
const handleEditCancel = () => {
|
|
1573
1746
|
showEditModal.value = false;
|
|
1574
1747
|
selectedItem.value = null;
|
|
1748
|
+
isCreateMode.value = false;
|
|
1575
1749
|
activeTabIndex.value = 0; // Reset tab for next time
|
|
1576
1750
|
if (editForm.value) {
|
|
1577
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
|
}
|