@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.
@@ -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.11",
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 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>
@@ -503,7 +511,7 @@
503
511
  <!-- Other field types with label -->
504
512
  <DFormGroup
505
513
  v-else
506
- :label="getField(fieldKey).label || fieldKey"
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="editFields"
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
- {{ 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>
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 || !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;
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
  }