@omnitend/dashboard-for-laravel 0.4.14 → 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 +10 -17
- package/dist/dashboard-for-laravel.js +8516 -8080
- 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 +114 -9
- 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 +35 -184
- 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 +96 -210
- package/resources/js/composables/defineForm.ts +7 -0
- package/resources/js/index.ts +12 -1
- package/resources/js/types/index.ts +146 -9
- package/resources/js/utils/objectPath.ts +59 -0
|
@@ -455,128 +455,70 @@
|
|
|
455
455
|
:title="computedModalTitle"
|
|
456
456
|
:size="editModalSize"
|
|
457
457
|
>
|
|
458
|
-
<!--
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
v-for="(tab, index) in visibleTabs"
|
|
463
|
-
:key="tab.key"
|
|
464
|
-
:title="tab.label || tab.key"
|
|
465
|
-
:lazy="tab.lazy"
|
|
466
|
-
:active="index === 0"
|
|
467
|
-
>
|
|
468
|
-
<!-- Custom tab content slot -->
|
|
469
|
-
<slot
|
|
470
|
-
v-if="$slots[`tab-content(${tab.key})`]"
|
|
471
|
-
:name="`tab-content(${tab.key})`"
|
|
472
|
-
:item="selectedItem"
|
|
473
|
-
:tab="tab"
|
|
474
|
-
/>
|
|
475
|
-
|
|
476
|
-
<!-- Default: render fields for this tab -->
|
|
477
|
-
<div v-else class="p-3">
|
|
478
|
-
<!-- Before slot -->
|
|
479
|
-
<slot :name="`tab-before(${tab.key})`" :item="selectedItem" :tab="tab" />
|
|
480
|
-
|
|
481
|
-
<!-- Form fields for this tab -->
|
|
482
|
-
<template v-for="fieldKey in tab.fieldKeys" :key="fieldKey">
|
|
483
|
-
<div v-if="getField(fieldKey).span" class="mb-3">
|
|
484
|
-
<!-- Full-width span field -->
|
|
485
|
-
<slot
|
|
486
|
-
:name="`edit-span(${fieldKey})`"
|
|
487
|
-
:item="selectedItem"
|
|
488
|
-
:value="editForm.data[fieldKey]"
|
|
489
|
-
:update="(v: any) => editForm.data[fieldKey] = v"
|
|
490
|
-
:close="handleEditCancel"
|
|
491
|
-
/>
|
|
492
|
-
</div>
|
|
493
|
-
<!-- Checkbox (no label wrapper needed) -->
|
|
494
|
-
<div v-else-if="getField(fieldKey).type === 'checkbox'" class="mb-3">
|
|
495
|
-
<!-- Custom value slot -->
|
|
496
|
-
<slot
|
|
497
|
-
v-if="$slots[`edit-value(${fieldKey})`]"
|
|
498
|
-
:name="`edit-value(${fieldKey})`"
|
|
499
|
-
:item="selectedItem"
|
|
500
|
-
:value="editForm.data[fieldKey]"
|
|
501
|
-
:update="(v: any) => editForm.data[fieldKey] = v"
|
|
502
|
-
:field="getField(fieldKey)"
|
|
503
|
-
/>
|
|
504
|
-
<DFormCheckbox
|
|
505
|
-
v-else
|
|
506
|
-
v-model="editForm.data[fieldKey]"
|
|
507
|
-
>
|
|
508
|
-
{{ getField(fieldKey).label || fieldKey }}
|
|
509
|
-
</DFormCheckbox>
|
|
510
|
-
</div>
|
|
511
|
-
<!-- Other field types with label -->
|
|
512
|
-
<DFormGroup
|
|
513
|
-
v-else
|
|
514
|
-
:label="getFieldLabel(fieldKey)"
|
|
515
|
-
class="mb-3"
|
|
516
|
-
>
|
|
517
|
-
<!-- Custom value slot -->
|
|
518
|
-
<slot
|
|
519
|
-
v-if="$slots[`edit-value(${fieldKey})`]"
|
|
520
|
-
:name="`edit-value(${fieldKey})`"
|
|
521
|
-
:item="selectedItem"
|
|
522
|
-
:value="editForm.data[fieldKey]"
|
|
523
|
-
:update="(v: any) => editForm.data[fieldKey] = v"
|
|
524
|
-
:field="getField(fieldKey)"
|
|
525
|
-
/>
|
|
526
|
-
<DFormTextarea
|
|
527
|
-
v-else-if="getField(fieldKey).type === 'textarea'"
|
|
528
|
-
v-model="editForm.data[fieldKey]"
|
|
529
|
-
:required="getField(fieldKey).required"
|
|
530
|
-
:rows="getField(fieldKey).rows || 3"
|
|
531
|
-
:state="editForm.getState(fieldKey)"
|
|
532
|
-
:disabled="isFieldDisabled(fieldKey)"
|
|
533
|
-
@input="editForm.clearError(fieldKey)"
|
|
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
|
-
/>
|
|
544
|
-
<DFormInput
|
|
545
|
-
v-else
|
|
546
|
-
v-model="editForm.data[fieldKey]"
|
|
547
|
-
:type="getField(fieldKey).type || 'text'"
|
|
548
|
-
:required="getField(fieldKey).required"
|
|
549
|
-
:step="getField(fieldKey).step"
|
|
550
|
-
:state="editForm.getState(fieldKey)"
|
|
551
|
-
:disabled="isFieldDisabled(fieldKey)"
|
|
552
|
-
@input="editForm.clearError(fieldKey)"
|
|
553
|
-
/>
|
|
554
|
-
<!-- Validation error -->
|
|
555
|
-
<DFormInvalidFeedback v-if="editForm.hasError(fieldKey)">
|
|
556
|
-
{{ editForm.getError(fieldKey) }}
|
|
557
|
-
</DFormInvalidFeedback>
|
|
558
|
-
<!-- Hint text -->
|
|
559
|
-
<DFormText v-if="getFieldHint(fieldKey)" class="text-muted">
|
|
560
|
-
{{ getFieldHint(fieldKey) }}
|
|
561
|
-
</DFormText>
|
|
562
|
-
</DFormGroup>
|
|
563
|
-
</template>
|
|
564
|
-
|
|
565
|
-
<!-- After slot -->
|
|
566
|
-
<slot :name="`tab-after(${tab.key})`" :item="selectedItem" :tab="tab" />
|
|
567
|
-
</div>
|
|
568
|
-
</DTab>
|
|
569
|
-
</DTabs>
|
|
570
|
-
</template>
|
|
571
|
-
|
|
572
|
-
<!-- Fallback: no tabs, render flat form (current behavior) -->
|
|
573
|
-
<DXBasicForm
|
|
574
|
-
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"
|
|
575
462
|
:form="editForm"
|
|
576
|
-
:fields="
|
|
463
|
+
:fields="editFields"
|
|
464
|
+
:tabs="editTabs"
|
|
465
|
+
:context="selectedItem ?? undefined"
|
|
577
466
|
:show-submit="false"
|
|
578
467
|
@submit="handleEditSave"
|
|
579
|
-
|
|
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>
|
|
580
522
|
|
|
581
523
|
<template #footer>
|
|
582
524
|
<div class="d-flex justify-content-between w-100">
|
|
@@ -614,7 +556,7 @@
|
|
|
614
556
|
</template>
|
|
615
557
|
|
|
616
558
|
<script setup lang="ts" generic="T = any">
|
|
617
|
-
import { computed, ref, watch } from "vue";
|
|
559
|
+
import { computed, ref, watch, useSlots } from "vue";
|
|
618
560
|
import { router } from "@inertiajs/vue3";
|
|
619
561
|
import axios from "axios";
|
|
620
562
|
import pluralize from "pluralize";
|
|
@@ -630,14 +572,7 @@ import DFormInput from "../base/DFormInput.vue";
|
|
|
630
572
|
import DFormSelect from "../base/DFormSelect.vue";
|
|
631
573
|
import DModal from "../base/DModal.vue";
|
|
632
574
|
import DButton from "../base/DButton.vue";
|
|
633
|
-
import
|
|
634
|
-
import DTab from "../base/DTab.vue";
|
|
635
|
-
import DFormGroup from "../base/DFormGroup.vue";
|
|
636
|
-
import DFormTextarea from "../base/DFormTextarea.vue";
|
|
637
|
-
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
638
|
-
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
639
|
-
import DFormText from "../base/DFormText.vue";
|
|
640
|
-
import DXBasicForm from "./DXBasicForm.vue";
|
|
575
|
+
import DXForm from "./DXForm.vue";
|
|
641
576
|
export type FilterType = 'text' | 'select' | 'number' | 'date' | false;
|
|
642
577
|
|
|
643
578
|
export interface FilterOption {
|
|
@@ -1455,67 +1390,35 @@ try {
|
|
|
1455
1390
|
createToast = undefined;
|
|
1456
1391
|
}
|
|
1457
1392
|
|
|
1458
|
-
//
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
});
|
|
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
|
+
);
|
|
1519
1422
|
|
|
1520
1423
|
// Computed: Singular and plural item names
|
|
1521
1424
|
const singularItemName = computed(() => props.itemName);
|
|
@@ -1653,16 +1556,8 @@ const handleEditSave = async () => {
|
|
|
1653
1556
|
modelValue: 5000,
|
|
1654
1557
|
});
|
|
1655
1558
|
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1559
|
+
// DXForm switches to the first errored tab via its
|
|
1560
|
+
// own watcher on editForm.errors.
|
|
1666
1561
|
emit('createError', errors);
|
|
1667
1562
|
}
|
|
1668
1563
|
});
|
|
@@ -1716,17 +1611,8 @@ const handleEditSave = async () => {
|
|
|
1716
1611
|
modelValue: 5000, // Auto-dismiss after 5 seconds
|
|
1717
1612
|
});
|
|
1718
1613
|
|
|
1719
|
-
//
|
|
1720
|
-
|
|
1721
|
-
const errorKeys = Object.keys(errors);
|
|
1722
|
-
const tabIndex = visibleTabs.value.findIndex(tab =>
|
|
1723
|
-
tab.fieldKeys.some(key => errorKeys.includes(key))
|
|
1724
|
-
);
|
|
1725
|
-
if (tabIndex !== -1) {
|
|
1726
|
-
activeTabIndex.value = tabIndex;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1614
|
+
// DXForm switches to the first errored tab via its
|
|
1615
|
+
// own watcher on editForm.errors.
|
|
1730
1616
|
emit('editError', selectedItem.value as T, errors);
|
|
1731
1617
|
}
|
|
1732
1618
|
});
|
|
@@ -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 {
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Field types supported by
|
|
4
|
+
* Field types supported by DXForm (and DXField, its per-field renderer).
|
|
5
|
+
*
|
|
6
|
+
* Text-like types render an `<input>`; the remainder render purpose-built
|
|
7
|
+
* controls:
|
|
8
|
+
* - `currency` / `percentage` — numeric input wrapped in an input-group
|
|
9
|
+
* with a symbol affix.
|
|
10
|
+
* - `datetime` — alias for the native `datetime-local` control.
|
|
11
|
+
* - `image` / `file` — file input (`image` additionally shows a preview).
|
|
12
|
+
* - `component` — escape hatch that renders `field.component`.
|
|
13
|
+
* - `repeater` — nested, repeatable sub-form driven by `field.fields`.
|
|
3
14
|
*/
|
|
4
15
|
export type FieldType =
|
|
5
16
|
| "text"
|
|
@@ -10,11 +21,25 @@ export type FieldType =
|
|
|
10
21
|
| "tel"
|
|
11
22
|
| "date"
|
|
12
23
|
| "datetime-local"
|
|
24
|
+
| "datetime"
|
|
13
25
|
| "time"
|
|
26
|
+
| "currency"
|
|
27
|
+
| "percentage"
|
|
14
28
|
| "textarea"
|
|
15
29
|
| "select"
|
|
16
30
|
| "checkbox"
|
|
17
|
-
| "radio"
|
|
31
|
+
| "radio"
|
|
32
|
+
| "image"
|
|
33
|
+
| "file"
|
|
34
|
+
| "component"
|
|
35
|
+
| "repeater";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A value that may be supplied directly or computed from the live form
|
|
39
|
+
* model. Predicates receive the current model so fields can react to
|
|
40
|
+
* other fields (cross-field reactivity).
|
|
41
|
+
*/
|
|
42
|
+
export type MaybeFn<TValue> = TValue | ((model: any) => TValue);
|
|
18
43
|
|
|
19
44
|
/**
|
|
20
45
|
* Option for select or radio fields
|
|
@@ -26,7 +51,18 @@ export interface FieldOption extends Record<string, unknown> {
|
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
/**
|
|
29
|
-
*
|
|
54
|
+
* Asynchronously resolves the options for a select/radio field from the
|
|
55
|
+
* current model (e.g. fetch a dependent list). Resolved on mount, and
|
|
56
|
+
* again on model change when `reloadOptionsOnChange` is set.
|
|
57
|
+
*/
|
|
58
|
+
export type OptionsLoader = (model: any) => Promise<FieldOption[]>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Field definition shared by every form renderer.
|
|
62
|
+
*
|
|
63
|
+
* DXForm and DXTable's edit modal honour `hint`, `span`, `when`,
|
|
64
|
+
* `readonly`, `disabledWhen`, function-valued `label`/`hint`, async
|
|
65
|
+
* options, the `component` escape hatch and nested `repeater` fields.
|
|
30
66
|
*/
|
|
31
67
|
export interface FieldDefinition {
|
|
32
68
|
/** Field key (must match form data key) */
|
|
@@ -35,8 +71,8 @@ export interface FieldDefinition {
|
|
|
35
71
|
/** Field type */
|
|
36
72
|
type: FieldType;
|
|
37
73
|
|
|
38
|
-
/** Field label
|
|
39
|
-
label?: string
|
|
74
|
+
/** Field label — string or a function of the form model */
|
|
75
|
+
label?: MaybeFn<string>;
|
|
40
76
|
|
|
41
77
|
/** Placeholder text (optional) */
|
|
42
78
|
placeholder?: string;
|
|
@@ -47,12 +83,40 @@ export interface FieldDefinition {
|
|
|
47
83
|
/** Options for select or radio fields */
|
|
48
84
|
options?: FieldOption[];
|
|
49
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Asynchronously load options for select/radio fields. Takes
|
|
88
|
+
* precedence over `options` once resolved.
|
|
89
|
+
*/
|
|
90
|
+
optionsLoader?: OptionsLoader;
|
|
91
|
+
|
|
92
|
+
/** Re-run `optionsLoader` whenever the form model changes. */
|
|
93
|
+
reloadOptionsOnChange?: boolean;
|
|
94
|
+
|
|
50
95
|
/** Number of rows for textarea (default: 3) */
|
|
51
96
|
rows?: number;
|
|
52
97
|
|
|
53
|
-
/**
|
|
98
|
+
/** Step for numeric/currency/percentage inputs */
|
|
99
|
+
step?: number | string;
|
|
100
|
+
|
|
101
|
+
/** Min/max for numeric inputs */
|
|
102
|
+
min?: number | string;
|
|
103
|
+
max?: number | string;
|
|
104
|
+
|
|
105
|
+
/** Symbol shown for `currency` fields (default: the locale's, "£"). */
|
|
106
|
+
currencySymbol?: string;
|
|
107
|
+
|
|
108
|
+
/** `accept` attribute for `image`/`file` inputs (e.g. "image/*"). */
|
|
109
|
+
accept?: string;
|
|
110
|
+
|
|
111
|
+
/** Help text displayed below the field (always visible). */
|
|
54
112
|
help?: string;
|
|
55
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Hint text displayed below the field. Unlike `help`, may be a
|
|
116
|
+
* function of the model for dynamic hints.
|
|
117
|
+
*/
|
|
118
|
+
hint?: MaybeFn<string>;
|
|
119
|
+
|
|
56
120
|
/** CSS class for the form group */
|
|
57
121
|
class?: string;
|
|
58
122
|
|
|
@@ -60,9 +124,82 @@ export interface FieldDefinition {
|
|
|
60
124
|
inputProps?: Record<string, any>;
|
|
61
125
|
|
|
62
126
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
127
|
+
* Render the field full-width with no label wrapper, delegating its
|
|
128
|
+
* content to the `#span(<key>)` slot. Useful for custom blocks.
|
|
129
|
+
*/
|
|
130
|
+
span?: boolean;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Component rendered for `type: "component"` fields. Receives
|
|
134
|
+
* `modelValue`, `field`, `model` props and emits `update:modelValue`.
|
|
135
|
+
*/
|
|
136
|
+
component?: Component;
|
|
137
|
+
|
|
138
|
+
/** Sub-field definitions for `type: "repeater"` fields. */
|
|
139
|
+
fields?: FieldDefinition[];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Default/initial value. Used by `defineForm` to seed form data and by
|
|
143
|
+
* repeaters to seed a freshly-added row's sub-fields. (`defineForm`'s
|
|
144
|
+
* `FormFieldDefinition` re-declares this as required for inference.)
|
|
145
|
+
*/
|
|
146
|
+
default?: any;
|
|
147
|
+
|
|
148
|
+
/** Label for a repeater's "add row" button (default: "Add"). */
|
|
149
|
+
addLabel?: string;
|
|
150
|
+
|
|
151
|
+
/** Minimum / maximum number of repeater rows. */
|
|
152
|
+
minItems?: number;
|
|
153
|
+
maxItems?: number;
|
|
154
|
+
|
|
155
|
+
/** Disable the field (static or computed from the model). */
|
|
156
|
+
disabled?: MaybeFn<boolean>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Disable the field based on the model. Retained for backwards
|
|
160
|
+
* compatibility with DXTable; prefer `disabled` with a function.
|
|
161
|
+
*/
|
|
162
|
+
disabledWhen?: (model: any) => boolean;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render the field read-only (static or computed). For controls
|
|
166
|
+
* without a native readonly state (select/checkbox/radio) this is
|
|
167
|
+
* applied as `disabled`.
|
|
168
|
+
*/
|
|
169
|
+
readonly?: MaybeFn<boolean>;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Conditionally show or hide this field. When omitted the field is
|
|
173
|
+
* always visible. Boolean or a function of the form model; evaluated
|
|
174
|
+
* reactively for cross-field conditional fields.
|
|
175
|
+
*/
|
|
176
|
+
when?: MaybeFn<boolean>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Legacy no-argument visibility predicate. Retained for backwards
|
|
180
|
+
* compatibility; prefer `when`. When both are present, a field is
|
|
181
|
+
* visible only if both pass.
|
|
66
182
|
*/
|
|
67
183
|
show?: () => boolean;
|
|
68
184
|
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A tab in a tabbed form. Groups a subset of fields and can be shown
|
|
188
|
+
* conditionally or lazily mounted.
|
|
189
|
+
*/
|
|
190
|
+
export interface FormTab {
|
|
191
|
+
/** Unique key for this tab */
|
|
192
|
+
key: string;
|
|
193
|
+
|
|
194
|
+
/** Display label (optional, defaults to the key) */
|
|
195
|
+
label?: string;
|
|
196
|
+
|
|
197
|
+
/** Field keys (from the form's fields) to render in this tab */
|
|
198
|
+
fieldKeys: string[];
|
|
199
|
+
|
|
200
|
+
/** Conditional display — boolean or a function of the form model */
|
|
201
|
+
when?: MaybeFn<boolean>;
|
|
202
|
+
|
|
203
|
+
/** Lazily mount tab content until first activated */
|
|
204
|
+
lazy?: boolean;
|
|
205
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal dot-path get/set helpers used for nested form binding
|
|
3
|
+
* (e.g. repeater rows where a field value lives at `lines.0.price`).
|
|
4
|
+
*
|
|
5
|
+
* Paths are dot-separated; numeric segments index into arrays. Setting
|
|
6
|
+
* creates intermediate objects/arrays as needed so a fresh repeater row
|
|
7
|
+
* can be populated without pre-seeding the whole shape.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type AnyRecord = Record<string, any>;
|
|
11
|
+
|
|
12
|
+
/** Segments that could pollute Object.prototype if written through. */
|
|
13
|
+
const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
14
|
+
|
|
15
|
+
/** Split a dot path into segments, ignoring empty segments. */
|
|
16
|
+
function splitPath(path: string): string[] {
|
|
17
|
+
return path.split(".").filter((segment) => segment.length > 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read the value at `path` within `target`, or `undefined` if absent. */
|
|
21
|
+
export function getByPath(target: AnyRecord, path: string): any {
|
|
22
|
+
const segments = splitPath(path);
|
|
23
|
+
let current: any = target;
|
|
24
|
+
for (const segment of segments) {
|
|
25
|
+
if (current === null || current === undefined) return undefined;
|
|
26
|
+
current = current[segment];
|
|
27
|
+
}
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write `value` at `path` within `target`, creating intermediate
|
|
33
|
+
* containers. A numeric next-segment creates an array; otherwise an
|
|
34
|
+
* object. Mutates `target` in place (form.data is reactive).
|
|
35
|
+
*/
|
|
36
|
+
export function setByPath(target: AnyRecord, path: string, value: any): void {
|
|
37
|
+
const segments = splitPath(path);
|
|
38
|
+
if (segments.length === 0) return;
|
|
39
|
+
// Never write through prototype-polluting segments.
|
|
40
|
+
if (segments.some((segment) => BLOCKED_SEGMENTS.has(segment))) return;
|
|
41
|
+
|
|
42
|
+
let current: any = target;
|
|
43
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
44
|
+
const segment = segments[index];
|
|
45
|
+
const nextSegment = segments[index + 1];
|
|
46
|
+
const nextIsIndex = /^\d+$/.test(nextSegment);
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
current[segment] === null ||
|
|
50
|
+
current[segment] === undefined ||
|
|
51
|
+
typeof current[segment] !== "object"
|
|
52
|
+
) {
|
|
53
|
+
current[segment] = nextIsIndex ? [] : {};
|
|
54
|
+
}
|
|
55
|
+
current = current[segment];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
current[segments[segments.length - 1]] = value;
|
|
59
|
+
}
|