@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.
@@ -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
- <!-- Tabbed view (if editTabs provided) -->
451
- <template v-if="editTabs && editTabs.length > 0 && editForm">
452
- <DTabs v-model="activeTabIndex">
453
- <DTab
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="resolvedEditFields"
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
- {{ editForm?.processing ? 'Saving...' : 'Save Changes' }}
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 DTabs from "../base/DTabs.vue";
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
- // Computed: Visible tabs (respects when condition)
1440
- const visibleTabs = computed(() => {
1441
- if (!props.editTabs || props.editTabs.length === 0) return [];
1442
-
1443
- return props.editTabs.filter(tab => {
1444
- if (tab.when === undefined) return true;
1445
- return typeof tab.when === 'function'
1446
- ? tab.when(selectedItem.value)
1447
- : tab.when;
1448
- });
1449
- });
1450
-
1451
- // Helper: Get field by key
1452
- const getField = (key: string) => {
1453
- return props.editFields?.find(f => f.key === key) || { key };
1454
- };
1455
-
1456
- // Helper: Get field label (supports function for dynamic labels)
1457
- const getFieldLabel = (key: string): string => {
1458
- const field = getField(key);
1459
- if (typeof field.label === 'function') {
1460
- return field.label(selectedItem.value);
1461
- }
1462
- return field.label || key;
1463
- };
1464
-
1465
- // Helper: Get field hint (supports function for dynamic hints)
1466
- const getFieldHint = (key: string): string | undefined => {
1467
- const field = getField(key);
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 || !selectedItem.value) return;
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
- // Switch to tab containing error field
1609
- if (props.editTabs && props.editTabs.length > 0) {
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 "";
@@ -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 {