@omnitend/dashboard-for-laravel 0.4.13 → 0.5.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.
- package/dist/components/base/DFormRadioGroup.vue.d.ts +12 -0
- package/dist/components/extended/DXBasicForm.vue.d.ts +4 -33
- package/dist/components/extended/DXField.vue.d.ts +88 -0
- package/dist/components/extended/DXForm.vue.d.ts +34 -8
- package/dist/components/extended/DXRepeater.vue.d.ts +30 -0
- package/dist/components/extended/DXTable.vue.d.ts +16 -19
- package/dist/dashboard-for-laravel.js +8023 -7517
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +117 -6
- package/dist/utils/objectPath.d.ts +18 -0
- package/docs/public/api-reference.json +345 -85
- package/docs/public/docs-map.md +5 -4
- package/docs/public/llms.txt +8 -5
- package/package.json +1 -1
- package/resources/js/components/base/DFormRadioGroup.vue +21 -0
- package/resources/js/components/extended/DXBasicForm.vue +36 -173
- package/resources/js/components/extended/DXField.vue +402 -0
- package/resources/js/components/extended/DXForm.vue +282 -17
- package/resources/js/components/extended/DXRepeater.vue +216 -0
- package/resources/js/components/extended/DXTable.vue +202 -204
- package/resources/js/composables/defineForm.ts +7 -0
- package/resources/js/index.ts +12 -1
- package/resources/js/types/index.ts +150 -6
- package/resources/js/utils/objectPath.ts +59 -0
|
@@ -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>
|
|
@@ -447,134 +455,76 @@
|
|
|
447
455
|
:title="computedModalTitle"
|
|
448
456
|
:size="editModalSize"
|
|
449
457
|
>
|
|
450
|
-
<!--
|
|
451
|
-
<
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
v-for="(tab, index) in visibleTabs"
|
|
455
|
-
:key="tab.key"
|
|
456
|
-
:title="tab.label || tab.key"
|
|
457
|
-
:lazy="tab.lazy"
|
|
458
|
-
:active="index === 0"
|
|
459
|
-
>
|
|
460
|
-
<!-- Custom tab content slot -->
|
|
461
|
-
<slot
|
|
462
|
-
v-if="$slots[`tab-content(${tab.key})`]"
|
|
463
|
-
:name="`tab-content(${tab.key})`"
|
|
464
|
-
:item="selectedItem"
|
|
465
|
-
:tab="tab"
|
|
466
|
-
/>
|
|
467
|
-
|
|
468
|
-
<!-- Default: render fields for this tab -->
|
|
469
|
-
<div v-else class="p-3">
|
|
470
|
-
<!-- Before slot -->
|
|
471
|
-
<slot :name="`tab-before(${tab.key})`" :item="selectedItem" :tab="tab" />
|
|
472
|
-
|
|
473
|
-
<!-- Form fields for this tab -->
|
|
474
|
-
<template v-for="fieldKey in tab.fieldKeys" :key="fieldKey">
|
|
475
|
-
<div v-if="getField(fieldKey).span" class="mb-3">
|
|
476
|
-
<!-- Full-width span field -->
|
|
477
|
-
<slot
|
|
478
|
-
:name="`edit-span(${fieldKey})`"
|
|
479
|
-
:item="selectedItem"
|
|
480
|
-
:value="editForm.data[fieldKey]"
|
|
481
|
-
:update="(v: any) => editForm.data[fieldKey] = v"
|
|
482
|
-
:close="handleEditCancel"
|
|
483
|
-
/>
|
|
484
|
-
</div>
|
|
485
|
-
<!-- Checkbox (no label wrapper needed) -->
|
|
486
|
-
<div v-else-if="getField(fieldKey).type === 'checkbox'" class="mb-3">
|
|
487
|
-
<!-- Custom value slot -->
|
|
488
|
-
<slot
|
|
489
|
-
v-if="$slots[`edit-value(${fieldKey})`]"
|
|
490
|
-
:name="`edit-value(${fieldKey})`"
|
|
491
|
-
:item="selectedItem"
|
|
492
|
-
:value="editForm.data[fieldKey]"
|
|
493
|
-
:update="(v: any) => editForm.data[fieldKey] = v"
|
|
494
|
-
:field="getField(fieldKey)"
|
|
495
|
-
/>
|
|
496
|
-
<DFormCheckbox
|
|
497
|
-
v-else
|
|
498
|
-
v-model="editForm.data[fieldKey]"
|
|
499
|
-
>
|
|
500
|
-
{{ getField(fieldKey).label || fieldKey }}
|
|
501
|
-
</DFormCheckbox>
|
|
502
|
-
</div>
|
|
503
|
-
<!-- Other field types with label -->
|
|
504
|
-
<DFormGroup
|
|
505
|
-
v-else
|
|
506
|
-
:label="getFieldLabel(fieldKey)"
|
|
507
|
-
class="mb-3"
|
|
508
|
-
>
|
|
509
|
-
<!-- Custom value slot -->
|
|
510
|
-
<slot
|
|
511
|
-
v-if="$slots[`edit-value(${fieldKey})`]"
|
|
512
|
-
:name="`edit-value(${fieldKey})`"
|
|
513
|
-
:item="selectedItem"
|
|
514
|
-
:value="editForm.data[fieldKey]"
|
|
515
|
-
:update="(v: any) => editForm.data[fieldKey] = v"
|
|
516
|
-
:field="getField(fieldKey)"
|
|
517
|
-
/>
|
|
518
|
-
<DFormTextarea
|
|
519
|
-
v-else-if="getField(fieldKey).type === 'textarea'"
|
|
520
|
-
v-model="editForm.data[fieldKey]"
|
|
521
|
-
:required="getField(fieldKey).required"
|
|
522
|
-
:rows="getField(fieldKey).rows || 3"
|
|
523
|
-
:state="editForm.getState(fieldKey)"
|
|
524
|
-
:disabled="isFieldDisabled(fieldKey)"
|
|
525
|
-
@input="editForm.clearError(fieldKey)"
|
|
526
|
-
/>
|
|
527
|
-
<DFormSelect
|
|
528
|
-
v-else-if="getField(fieldKey).type === 'select'"
|
|
529
|
-
v-model="editForm.data[fieldKey]"
|
|
530
|
-
:required="getField(fieldKey).required"
|
|
531
|
-
:options="getField(fieldKey).options"
|
|
532
|
-
:state="editForm.getState(fieldKey)"
|
|
533
|
-
:disabled="isFieldDisabled(fieldKey)"
|
|
534
|
-
@change="editForm.clearError(fieldKey)"
|
|
535
|
-
/>
|
|
536
|
-
<DFormInput
|
|
537
|
-
v-else
|
|
538
|
-
v-model="editForm.data[fieldKey]"
|
|
539
|
-
:type="getField(fieldKey).type || 'text'"
|
|
540
|
-
:required="getField(fieldKey).required"
|
|
541
|
-
:step="getField(fieldKey).step"
|
|
542
|
-
:state="editForm.getState(fieldKey)"
|
|
543
|
-
:disabled="isFieldDisabled(fieldKey)"
|
|
544
|
-
@input="editForm.clearError(fieldKey)"
|
|
545
|
-
/>
|
|
546
|
-
<!-- Validation error -->
|
|
547
|
-
<DFormInvalidFeedback v-if="editForm.hasError(fieldKey)">
|
|
548
|
-
{{ editForm.getError(fieldKey) }}
|
|
549
|
-
</DFormInvalidFeedback>
|
|
550
|
-
<!-- Hint text -->
|
|
551
|
-
<DFormText v-if="getFieldHint(fieldKey)" class="text-muted">
|
|
552
|
-
{{ getFieldHint(fieldKey) }}
|
|
553
|
-
</DFormText>
|
|
554
|
-
</DFormGroup>
|
|
555
|
-
</template>
|
|
556
|
-
|
|
557
|
-
<!-- After slot -->
|
|
558
|
-
<slot :name="`tab-after(${tab.key})`" :item="selectedItem" :tab="tab" />
|
|
559
|
-
</div>
|
|
560
|
-
</DTab>
|
|
561
|
-
</DTabs>
|
|
562
|
-
</template>
|
|
563
|
-
|
|
564
|
-
<!-- Fallback: no tabs, render flat form (current behavior) -->
|
|
565
|
-
<DXBasicForm
|
|
566
|
-
v-else-if="editForm"
|
|
458
|
+
<!-- Edit/create form (tabbed when editTabs provided, flat otherwise) -->
|
|
459
|
+
<DXForm
|
|
460
|
+
v-if="editForm"
|
|
461
|
+
v-model:active-tab="activeTabIndex"
|
|
567
462
|
:form="editForm"
|
|
568
|
-
:fields="
|
|
463
|
+
:fields="editFields"
|
|
464
|
+
:tabs="editTabs"
|
|
465
|
+
:context="selectedItem ?? undefined"
|
|
569
466
|
:show-submit="false"
|
|
570
467
|
@submit="handleEditSave"
|
|
571
|
-
|
|
468
|
+
>
|
|
469
|
+
<!-- Forward DXTable's edit-value(key) → DXForm value(key) -->
|
|
470
|
+
<template
|
|
471
|
+
v-for="key in editValueSlotKeys"
|
|
472
|
+
:key="`ev-${key}`"
|
|
473
|
+
#[`value(${key})`]="sp"
|
|
474
|
+
>
|
|
475
|
+
<slot
|
|
476
|
+
:name="`edit-value(${key})`"
|
|
477
|
+
:item="selectedItem"
|
|
478
|
+
:value="sp.value"
|
|
479
|
+
:update="sp.update"
|
|
480
|
+
:field="sp.field"
|
|
481
|
+
/>
|
|
482
|
+
</template>
|
|
483
|
+
|
|
484
|
+
<!-- Forward edit-span(key) → span(key) -->
|
|
485
|
+
<template
|
|
486
|
+
v-for="key in editSpanSlotKeys"
|
|
487
|
+
:key="`es-${key}`"
|
|
488
|
+
#[`span(${key})`]="sp"
|
|
489
|
+
>
|
|
490
|
+
<slot
|
|
491
|
+
:name="`edit-span(${key})`"
|
|
492
|
+
:item="selectedItem"
|
|
493
|
+
:value="sp.value"
|
|
494
|
+
:update="sp.update"
|
|
495
|
+
:close="handleEditCancel"
|
|
496
|
+
/>
|
|
497
|
+
</template>
|
|
498
|
+
|
|
499
|
+
<!-- Forward tab-content / tab-before / tab-after slots -->
|
|
500
|
+
<template
|
|
501
|
+
v-for="key in tabContentSlotKeys"
|
|
502
|
+
:key="`tc-${key}`"
|
|
503
|
+
#[`tab-content(${key})`]="sp"
|
|
504
|
+
>
|
|
505
|
+
<slot :name="`tab-content(${key})`" :item="selectedItem" :tab="sp.tab" />
|
|
506
|
+
</template>
|
|
507
|
+
<template
|
|
508
|
+
v-for="key in tabBeforeSlotKeys"
|
|
509
|
+
:key="`tb-${key}`"
|
|
510
|
+
#[`tab-before(${key})`]="sp"
|
|
511
|
+
>
|
|
512
|
+
<slot :name="`tab-before(${key})`" :item="selectedItem" :tab="sp.tab" />
|
|
513
|
+
</template>
|
|
514
|
+
<template
|
|
515
|
+
v-for="key in tabAfterSlotKeys"
|
|
516
|
+
:key="`taf-${key}`"
|
|
517
|
+
#[`tab-after(${key})`]="sp"
|
|
518
|
+
>
|
|
519
|
+
<slot :name="`tab-after(${key})`" :item="selectedItem" :tab="sp.tab" />
|
|
520
|
+
</template>
|
|
521
|
+
</DXForm>
|
|
572
522
|
|
|
573
523
|
<template #footer>
|
|
574
524
|
<div class="d-flex justify-content-between w-100">
|
|
575
525
|
<div>
|
|
576
526
|
<DButton
|
|
577
|
-
v-if="deleteUrl"
|
|
527
|
+
v-if="deleteUrl && !isCreateMode"
|
|
578
528
|
variant="danger"
|
|
579
529
|
:disabled="editForm?.processing"
|
|
580
530
|
@click="handleDelete"
|
|
@@ -591,7 +541,12 @@
|
|
|
591
541
|
:disabled="editForm?.processing"
|
|
592
542
|
@click="handleEditSave"
|
|
593
543
|
>
|
|
594
|
-
|
|
544
|
+
<template v-if="isCreateMode">
|
|
545
|
+
{{ editForm?.processing ? 'Creating...' : 'Create' }}
|
|
546
|
+
</template>
|
|
547
|
+
<template v-else>
|
|
548
|
+
{{ editForm?.processing ? 'Saving...' : 'Save Changes' }}
|
|
549
|
+
</template>
|
|
595
550
|
</DButton>
|
|
596
551
|
</div>
|
|
597
552
|
</div>
|
|
@@ -601,7 +556,7 @@
|
|
|
601
556
|
</template>
|
|
602
557
|
|
|
603
558
|
<script setup lang="ts" generic="T = any">
|
|
604
|
-
import { computed, ref, watch } from "vue";
|
|
559
|
+
import { computed, ref, watch, useSlots } from "vue";
|
|
605
560
|
import { router } from "@inertiajs/vue3";
|
|
606
561
|
import axios from "axios";
|
|
607
562
|
import pluralize from "pluralize";
|
|
@@ -617,14 +572,7 @@ import DFormInput from "../base/DFormInput.vue";
|
|
|
617
572
|
import DFormSelect from "../base/DFormSelect.vue";
|
|
618
573
|
import DModal from "../base/DModal.vue";
|
|
619
574
|
import DButton from "../base/DButton.vue";
|
|
620
|
-
import
|
|
621
|
-
import DTab from "../base/DTab.vue";
|
|
622
|
-
import DFormGroup from "../base/DFormGroup.vue";
|
|
623
|
-
import DFormTextarea from "../base/DFormTextarea.vue";
|
|
624
|
-
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
625
|
-
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
626
|
-
import DFormText from "../base/DFormText.vue";
|
|
627
|
-
import DXBasicForm from "./DXBasicForm.vue";
|
|
575
|
+
import DXForm from "./DXForm.vue";
|
|
628
576
|
export type FilterType = 'text' | 'select' | 'number' | 'date' | false;
|
|
629
577
|
|
|
630
578
|
export interface FilterOption {
|
|
@@ -784,6 +732,9 @@ export interface Props<TItem = any> {
|
|
|
784
732
|
/** API endpoint pattern for deletions (e.g., "/api/products/:id") */
|
|
785
733
|
deleteUrl?: string;
|
|
786
734
|
|
|
735
|
+
/** API endpoint for creating new items (e.g., "/api/products") — enables "New" button */
|
|
736
|
+
createUrl?: string;
|
|
737
|
+
|
|
787
738
|
/** Enable client-side filtering, sorting, and pagination on items array */
|
|
788
739
|
clientSide?: boolean;
|
|
789
740
|
}
|
|
@@ -821,6 +772,8 @@ const emit = defineEmits<{
|
|
|
821
772
|
filterChange: [filters: Record<string, string>];
|
|
822
773
|
perPageChange: [perPage: number];
|
|
823
774
|
rowClicked: [item: T, index: number, event: MouseEvent];
|
|
775
|
+
rowCreated: [item: any, response: any];
|
|
776
|
+
createError: [error: any];
|
|
824
777
|
rowUpdated: [item: T, response: any];
|
|
825
778
|
editError: [item: T, error: any];
|
|
826
779
|
rowDeleted: [item: T, response: any];
|
|
@@ -1425,6 +1378,7 @@ const showEditModal = ref(false);
|
|
|
1425
1378
|
const selectedItem = ref<T | null>(null);
|
|
1426
1379
|
const editForm = ref<any>(null);
|
|
1427
1380
|
const activeTabIndex = ref(0);
|
|
1381
|
+
const isCreateMode = ref(false);
|
|
1428
1382
|
|
|
1429
1383
|
// Toast (may not be available in test environment)
|
|
1430
1384
|
let createToast: ((obj: any) => any) | undefined;
|
|
@@ -1436,67 +1390,35 @@ try {
|
|
|
1436
1390
|
createToast = undefined;
|
|
1437
1391
|
}
|
|
1438
1392
|
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
if (typeof field.hint === 'function') {
|
|
1469
|
-
return field.hint(selectedItem.value);
|
|
1470
|
-
}
|
|
1471
|
-
return field.hint;
|
|
1472
|
-
};
|
|
1473
|
-
|
|
1474
|
-
// Helper: Check if field is disabled (supports disabledWhen function)
|
|
1475
|
-
const isFieldDisabled = (key: string): boolean => {
|
|
1476
|
-
const field = getField(key);
|
|
1477
|
-
if (typeof field.disabledWhen === 'function') {
|
|
1478
|
-
return field.disabledWhen(selectedItem.value);
|
|
1479
|
-
}
|
|
1480
|
-
return field.disabled || false;
|
|
1481
|
-
};
|
|
1482
|
-
|
|
1483
|
-
// Computed: Resolve edit fields with dynamic labels/hints for DXBasicForm
|
|
1484
|
-
const resolvedEditFields = computed(() => {
|
|
1485
|
-
if (!props.editFields) return [];
|
|
1486
|
-
|
|
1487
|
-
return props.editFields.map(field => ({
|
|
1488
|
-
...field,
|
|
1489
|
-
label: typeof field.label === 'function'
|
|
1490
|
-
? field.label(selectedItem.value)
|
|
1491
|
-
: field.label,
|
|
1492
|
-
hint: typeof field.hint === 'function'
|
|
1493
|
-
? field.hint(selectedItem.value)
|
|
1494
|
-
: field.hint,
|
|
1495
|
-
disabled: typeof field.disabledWhen === 'function'
|
|
1496
|
-
? field.disabledWhen(selectedItem.value)
|
|
1497
|
-
: field.disabled,
|
|
1498
|
-
}));
|
|
1499
|
-
});
|
|
1393
|
+
// The edit/create form rendering is delegated to DXForm, which
|
|
1394
|
+
// owns field/tab visibility, dynamic labels/hints, conditional fields,
|
|
1395
|
+
// and auto-switching to the first tab with a validation error.
|
|
1396
|
+
|
|
1397
|
+
// Forward only the keyed edit slots the consumer actually provided, so
|
|
1398
|
+
// DXForm doesn't mistake an always-present (but empty) wrapper for
|
|
1399
|
+
// a real custom-value override.
|
|
1400
|
+
const tableSlots = useSlots();
|
|
1401
|
+
const editFieldKeys = computed<string[]>(() =>
|
|
1402
|
+
(props.editFields ?? []).map((field: any) => field.key),
|
|
1403
|
+
);
|
|
1404
|
+
const tabKeys = computed<string[]>(() =>
|
|
1405
|
+
(props.editTabs ?? []).map((tab) => tab.key),
|
|
1406
|
+
);
|
|
1407
|
+
const editValueSlotKeys = computed(() =>
|
|
1408
|
+
editFieldKeys.value.filter((key) => !!tableSlots[`edit-value(${key})`]),
|
|
1409
|
+
);
|
|
1410
|
+
const editSpanSlotKeys = computed(() =>
|
|
1411
|
+
editFieldKeys.value.filter((key) => !!tableSlots[`edit-span(${key})`]),
|
|
1412
|
+
);
|
|
1413
|
+
const tabContentSlotKeys = computed(() =>
|
|
1414
|
+
tabKeys.value.filter((key) => !!tableSlots[`tab-content(${key})`]),
|
|
1415
|
+
);
|
|
1416
|
+
const tabBeforeSlotKeys = computed(() =>
|
|
1417
|
+
tabKeys.value.filter((key) => !!tableSlots[`tab-before(${key})`]),
|
|
1418
|
+
);
|
|
1419
|
+
const tabAfterSlotKeys = computed(() =>
|
|
1420
|
+
tabKeys.value.filter((key) => !!tableSlots[`tab-after(${key})`]),
|
|
1421
|
+
);
|
|
1500
1422
|
|
|
1501
1423
|
// Computed: Singular and plural item names
|
|
1502
1424
|
const singularItemName = computed(() => props.itemName);
|
|
@@ -1504,6 +1426,9 @@ const pluralItemName = computed(() => pluralize(props.itemName));
|
|
|
1504
1426
|
|
|
1505
1427
|
// Computed: Modal title (supports function)
|
|
1506
1428
|
const computedModalTitle = computed(() => {
|
|
1429
|
+
if (isCreateMode.value) {
|
|
1430
|
+
return `New ${singularItemName.value}`;
|
|
1431
|
+
}
|
|
1507
1432
|
if (!selectedItem.value) {
|
|
1508
1433
|
return `Edit ${singularItemName.value}`;
|
|
1509
1434
|
}
|
|
@@ -1529,6 +1454,7 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1529
1454
|
// If editFields provided, open edit modal
|
|
1530
1455
|
if (props.editFields && props.editFields.length > 0) {
|
|
1531
1456
|
// Set selected item FIRST before any rendering
|
|
1457
|
+
isCreateMode.value = false;
|
|
1532
1458
|
selectedItem.value = item;
|
|
1533
1459
|
|
|
1534
1460
|
// Reset to first tab
|
|
@@ -1560,9 +1486,89 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
|
|
|
1560
1486
|
}
|
|
1561
1487
|
};
|
|
1562
1488
|
|
|
1489
|
+
// Handle "New" button click
|
|
1490
|
+
const handleCreateNew = () => {
|
|
1491
|
+
if (!props.editFields || props.editFields.length === 0) return;
|
|
1492
|
+
|
|
1493
|
+
isCreateMode.value = true;
|
|
1494
|
+
selectedItem.value = null;
|
|
1495
|
+
activeTabIndex.value = 0;
|
|
1496
|
+
|
|
1497
|
+
const initForm = (useForm: any) => {
|
|
1498
|
+
const formData: Record<string, any> = {};
|
|
1499
|
+
props.editFields!.forEach(field => {
|
|
1500
|
+
formData[field.key] = field.default ?? '';
|
|
1501
|
+
});
|
|
1502
|
+
editForm.value = useForm(formData);
|
|
1503
|
+
showEditModal.value = true;
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
if (!editForm.value) {
|
|
1507
|
+
import('../../composables/useForm').then(({ useForm }) => {
|
|
1508
|
+
initForm(useForm);
|
|
1509
|
+
});
|
|
1510
|
+
} else {
|
|
1511
|
+
// Reset existing form to defaults
|
|
1512
|
+
props.editFields!.forEach(field => {
|
|
1513
|
+
editForm.value.data[field.key] = field.default ?? '';
|
|
1514
|
+
});
|
|
1515
|
+
editForm.value.clearErrors();
|
|
1516
|
+
showEditModal.value = true;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1563
1520
|
// Handle save from edit modal
|
|
1564
1521
|
const handleEditSave = async () => {
|
|
1565
|
-
if (!editForm.value
|
|
1522
|
+
if (!editForm.value) return;
|
|
1523
|
+
|
|
1524
|
+
// Create mode: POST to createUrl
|
|
1525
|
+
if (isCreateMode.value && props.createUrl) {
|
|
1526
|
+
try {
|
|
1527
|
+
await editForm.value.post(props.createUrl, {
|
|
1528
|
+
onSuccess: (data: any) => {
|
|
1529
|
+
createToast?.({
|
|
1530
|
+
title: 'Success',
|
|
1531
|
+
body: `${singularItemName.value} created successfully`,
|
|
1532
|
+
variant: 'success',
|
|
1533
|
+
modelValue: 3000,
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
emit('rowCreated', data?.data ?? data, data);
|
|
1537
|
+
showEditModal.value = false;
|
|
1538
|
+
selectedItem.value = null;
|
|
1539
|
+
isCreateMode.value = false;
|
|
1540
|
+
|
|
1541
|
+
refresh();
|
|
1542
|
+
},
|
|
1543
|
+
onError: (errors: any) => {
|
|
1544
|
+
let errorMessage = 'Failed to create. Please check the form for errors.';
|
|
1545
|
+
if (errors && typeof errors === 'object') {
|
|
1546
|
+
const firstError = Object.values(errors).flat()[0];
|
|
1547
|
+
if (typeof firstError === 'string') {
|
|
1548
|
+
errorMessage = firstError;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
createToast?.({
|
|
1553
|
+
title: 'Error',
|
|
1554
|
+
body: errorMessage,
|
|
1555
|
+
variant: 'danger',
|
|
1556
|
+
modelValue: 5000,
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
// DXForm switches to the first errored tab via its
|
|
1560
|
+
// own watcher on editForm.errors.
|
|
1561
|
+
emit('createError', errors);
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
emit('createError', error);
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Edit mode: PUT to editUrl
|
|
1571
|
+
if (!selectedItem.value) return;
|
|
1566
1572
|
|
|
1567
1573
|
try {
|
|
1568
1574
|
// If editUrl provided, handle API call internally
|
|
@@ -1605,17 +1611,8 @@ const handleEditSave = async () => {
|
|
|
1605
1611
|
modelValue: 5000, // Auto-dismiss after 5 seconds
|
|
1606
1612
|
});
|
|
1607
1613
|
|
|
1608
|
-
//
|
|
1609
|
-
|
|
1610
|
-
const errorKeys = Object.keys(errors);
|
|
1611
|
-
const tabIndex = visibleTabs.value.findIndex(tab =>
|
|
1612
|
-
tab.fieldKeys.some(key => errorKeys.includes(key))
|
|
1613
|
-
);
|
|
1614
|
-
if (tabIndex !== -1) {
|
|
1615
|
-
activeTabIndex.value = tabIndex;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1614
|
+
// DXForm switches to the first errored tab via its
|
|
1615
|
+
// own watcher on editForm.errors.
|
|
1619
1616
|
emit('editError', selectedItem.value as T, errors);
|
|
1620
1617
|
}
|
|
1621
1618
|
});
|
|
@@ -1634,6 +1631,7 @@ const handleEditSave = async () => {
|
|
|
1634
1631
|
const handleEditCancel = () => {
|
|
1635
1632
|
showEditModal.value = false;
|
|
1636
1633
|
selectedItem.value = null;
|
|
1634
|
+
isCreateMode.value = false;
|
|
1637
1635
|
activeTabIndex.value = 0; // Reset tab for next time
|
|
1638
1636
|
if (editForm.value) {
|
|
1639
1637
|
editForm.value.clearErrors();
|
|
@@ -32,7 +32,14 @@ function getDefaultValueForType(type: FieldType): any {
|
|
|
32
32
|
case "checkbox":
|
|
33
33
|
return false;
|
|
34
34
|
case "number":
|
|
35
|
+
case "currency":
|
|
36
|
+
case "percentage":
|
|
35
37
|
return 0;
|
|
38
|
+
case "repeater":
|
|
39
|
+
return [];
|
|
40
|
+
case "image":
|
|
41
|
+
case "file":
|
|
42
|
+
return null;
|
|
36
43
|
case "select":
|
|
37
44
|
case "radio":
|
|
38
45
|
return "";
|
package/resources/js/index.ts
CHANGED
|
@@ -6,8 +6,15 @@ export * from 'bootstrap-vue-next';
|
|
|
6
6
|
|
|
7
7
|
// Extended components (custom functionality beyond Bootstrap Vue Next)
|
|
8
8
|
export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
|
|
9
|
-
export { default as DXBasicForm } from "./components/extended/DXBasicForm.vue";
|
|
10
9
|
export { default as DXForm } from "./components/extended/DXForm.vue";
|
|
10
|
+
export { default as DXField } from "./components/extended/DXField.vue";
|
|
11
|
+
export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Use `DXForm`. `DXBasicForm` is a thin wrapper around `DXForm`
|
|
14
|
+
* (a flat form is just `DXForm` without a `tabs` prop) that logs a one-time
|
|
15
|
+
* deprecation warning. It will be removed in a future major version.
|
|
16
|
+
*/
|
|
17
|
+
export { default as DXBasicForm } from "./components/extended/DXBasicForm.vue";
|
|
11
18
|
export { default as DXTable } from "./components/extended/DXTable.vue";
|
|
12
19
|
export { default as DXDashboardSidebar } from "./components/extended/DXDashboardSidebar.vue";
|
|
13
20
|
export { default as DXDashboardNavbar } from "./components/extended/DXDashboardNavbar.vue";
|
|
@@ -37,6 +44,7 @@ export { default as DFormGroup } from "./components/base/DFormGroup.vue";
|
|
|
37
44
|
export { default as DFormInput } from "./components/base/DFormInput.vue";
|
|
38
45
|
export { default as DFormInvalidFeedback } from "./components/base/DFormInvalidFeedback.vue";
|
|
39
46
|
export { default as DFormRadio } from "./components/base/DFormRadio.vue";
|
|
47
|
+
export { default as DFormRadioGroup } from "./components/base/DFormRadioGroup.vue";
|
|
40
48
|
export { default as DFormSelect } from "./components/base/DFormSelect.vue";
|
|
41
49
|
export { default as DFormSpinbutton } from "./components/base/DFormSpinbutton.vue";
|
|
42
50
|
export { default as DFormTags } from "./components/base/DFormTags.vue";
|
|
@@ -82,6 +90,9 @@ export type {
|
|
|
82
90
|
FieldType,
|
|
83
91
|
FieldOption,
|
|
84
92
|
FieldDefinition,
|
|
93
|
+
FormTab,
|
|
94
|
+
MaybeFn,
|
|
95
|
+
OptionsLoader,
|
|
85
96
|
} from "./types";
|
|
86
97
|
|
|
87
98
|
export type {
|