@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.
@@ -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.13",
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",
@@ -11,7 +11,7 @@
11
11
  </DAlert>
12
12
 
13
13
  <!-- Render each field -->
14
- <template v-for="field in fields" :key="field.key">
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
- {{ editForm?.processing ? 'Saving...' : 'Save Changes' }}
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 || !selectedItem.value) return;
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
  }