@omnitend/dashboard-for-laravel 0.43.0 → 0.45.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/DFormSelect.vue.d.ts +1 -1
- package/dist/components/extended/DXChoiceField.vue.d.ts +3 -3
- package/dist/components/extended/DXForm.vue.d.ts +66 -1
- package/dist/components/extended/DXFormErrorSummary.vue.d.ts +70 -0
- package/dist/components/extended/DXFormField.vue.d.ts +3 -0
- package/dist/components/extended/DXLoading.vue.d.ts +3 -0
- package/dist/components/extended/DXTable.vue.d.ts +20 -0
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +14 -10
- package/dist/composables/useAsyncOptions.d.ts +14 -1
- package/dist/composables/useForm.d.ts +26 -0
- package/dist/dashboard-for-laravel.js +13605 -13058
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +12 -0
- package/dist/utils/formErrorTargets.d.ts +86 -0
- package/docs/public/api-reference.json +148 -8
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +9 -8
- package/resources/css/theme.scss +176 -0
- package/resources/js/components/base/DFormSelect.vue +5 -2
- package/resources/js/components/extended/DXChoiceField.vue +42 -26
- package/resources/js/components/extended/DXField.vue +51 -17
- package/resources/js/components/extended/DXForm.vue +676 -28
- package/resources/js/components/extended/DXFormErrorSummary.vue +232 -0
- package/resources/js/components/extended/DXFormField.vue +11 -2
- package/resources/js/components/extended/DXLoading.vue +18 -11
- package/resources/js/components/extended/DXRepeater.vue +30 -4
- package/resources/js/components/extended/DXTable.vue +38 -9
- package/resources/js/components/extended/DXTableEditorModal.vue +29 -1
- package/resources/js/composables/useAsyncOptions.ts +30 -5
- package/resources/js/composables/useForm.ts +196 -18
- package/resources/js/composables/useResourceEditor.ts +140 -107
- package/resources/js/index.ts +15 -0
- package/resources/js/types/index.ts +13 -0
- package/resources/js/utils/formErrorTargets.ts +317 -0
- package/scripts/mcp-server.mjs +58 -6
|
@@ -13,27 +13,26 @@
|
|
|
13
13
|
-->
|
|
14
14
|
<template>
|
|
15
15
|
<!-- Searchable select: type to filter a long option list, while the model
|
|
16
|
-
keeps the option's VALUE (an id), not the typed text.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</template>
|
|
16
|
+
keeps the option's VALUE (an id), not the typed text. Async options
|
|
17
|
+
can arrive after it mounts: DAutocomplete then relabels the input
|
|
18
|
+
(0.42.1), so the control mounts at once (#189). Until the first load
|
|
19
|
+
lands it holds the value back (it would show as the raw id, "51"
|
|
20
|
+
rather than "Waitrose") and reads "Loading…", disabled. The guard is
|
|
21
|
+
bound AFTER the consumer's controlProps so they cannot lift it, and
|
|
22
|
+
nothing the control emits while the value is held back reaches the
|
|
23
|
+
form: it would be an edit of the empty stand-in, not of the value. -->
|
|
24
|
+
<DAutocomplete
|
|
25
|
+
v-if="field.type === 'select' && field.searchable"
|
|
26
|
+
:options="options"
|
|
27
|
+
:placeholder="field.placeholder"
|
|
28
|
+
:required="field.required"
|
|
29
|
+
:state="state"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
open-on-focus
|
|
32
|
+
v-bind="{ ...controlProps, ...searchableLoadingGuard }"
|
|
33
|
+
:model-value="optionsPending ? heldBackValue : modelValue"
|
|
34
|
+
@update:model-value="onSearchableUpdate"
|
|
35
|
+
/>
|
|
37
36
|
|
|
38
37
|
<!-- Select (sync or async options) -->
|
|
39
38
|
<DFormSelect
|
|
@@ -73,10 +72,10 @@
|
|
|
73
72
|
</template>
|
|
74
73
|
|
|
75
74
|
<script setup lang="ts">
|
|
75
|
+
import { computed } from "vue";
|
|
76
76
|
import DFormSelect from "../base/DFormSelect.vue";
|
|
77
77
|
import DFormRadioGroup from "../base/DFormRadioGroup.vue";
|
|
78
78
|
import DFormCheckboxGroup from "../base/DFormCheckboxGroup.vue";
|
|
79
|
-
import DFormInput from "../base/DFormInput.vue";
|
|
80
79
|
import DAutocomplete from "../base/DAutocomplete.vue";
|
|
81
80
|
import type { FieldDefinition, FieldOption } from "../../types";
|
|
82
81
|
|
|
@@ -100,15 +99,32 @@ interface Props {
|
|
|
100
99
|
controlProps?: Record<string, any>;
|
|
101
100
|
|
|
102
101
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* The field's first async options load is still in flight. A searchable
|
|
103
|
+
* select then shows "Loading…" instead of its value's raw id.
|
|
105
104
|
*/
|
|
106
|
-
|
|
105
|
+
optionsPending?: boolean;
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
defineProps<Props>();
|
|
108
|
+
const props = defineProps<Props>();
|
|
110
109
|
|
|
111
110
|
const emit = defineEmits<{
|
|
112
111
|
(event: "update:modelValue", value: any): void;
|
|
113
112
|
}>();
|
|
113
|
+
|
|
114
|
+
// What the searchable select shows while its first options load: nothing
|
|
115
|
+
// selected, in the shape the model has (an array for `multiple`).
|
|
116
|
+
const heldBackValue = computed(() => (Array.isArray(props.modelValue) ? [] : null));
|
|
117
|
+
|
|
118
|
+
// Applied over the consumer's controlProps, so `inputProps: { disabled: false }`
|
|
119
|
+
// cannot re-enable the control while its value is held back.
|
|
120
|
+
const searchableLoadingGuard = computed<Record<string, unknown>>(() =>
|
|
121
|
+
props.optionsPending ? { disabled: true, placeholder: "Loading…" } : {},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const onSearchableUpdate = (value: unknown): void => {
|
|
125
|
+
// An update while the value is held back edits the empty stand-in, not the
|
|
126
|
+
// value (a pick in multiple mode would drop every existing selection).
|
|
127
|
+
if (props.optionsPending) return;
|
|
128
|
+
emit("update:modelValue", value);
|
|
129
|
+
};
|
|
114
130
|
</script>
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
<template>
|
|
14
14
|
<!-- Full-width span field: delegate entirely to the #span slot, bypassing
|
|
15
15
|
the label and built-in control (and the shell). -->
|
|
16
|
-
|
|
16
|
+
<!-- `data-dx-field-key` (the field's data path: `name`, or
|
|
17
|
+
`lines.0.price` inside a repeater) is how DXForm finds a field to
|
|
18
|
+
scroll to and focus after a failed submit (#194). -->
|
|
19
|
+
<div v-if="field.span" :class="field.class || 'mb-3'" :data-dx-field-key="valuePath">
|
|
17
20
|
<!--
|
|
18
21
|
@slot Replaces the entire field with custom full-width content when `field.span` is set, bypassing the label and built-in control.
|
|
19
22
|
@binding {FieldDefinition} field The field definition being rendered.
|
|
@@ -34,6 +37,7 @@
|
|
|
34
37
|
control that goes inside is chosen by field.type in the default slot. -->
|
|
35
38
|
<DXFieldShell
|
|
36
39
|
v-else
|
|
40
|
+
:data-dx-field-key="valuePath"
|
|
37
41
|
:field="field"
|
|
38
42
|
:form="form"
|
|
39
43
|
:error-key="errorKey"
|
|
@@ -264,7 +268,7 @@
|
|
|
264
268
|
:state="fieldState"
|
|
265
269
|
:disabled="isDisabled || isReadonly || isPlaintext"
|
|
266
270
|
:control-props="controlPropsWithGuards"
|
|
267
|
-
:options-
|
|
271
|
+
:options-pending="optionsPending"
|
|
268
272
|
@update:model-value="setValue"
|
|
269
273
|
/>
|
|
270
274
|
|
|
@@ -401,7 +405,14 @@ import DXNumericField from "./DXNumericField.vue";
|
|
|
401
405
|
import DXChoiceField from "./DXChoiceField.vue";
|
|
402
406
|
import { useAsyncOptions } from "../../composables/useAsyncOptions";
|
|
403
407
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
404
|
-
import type {
|
|
408
|
+
import type {
|
|
409
|
+
FieldDefinition,
|
|
410
|
+
FieldOption,
|
|
411
|
+
FieldType,
|
|
412
|
+
LabelCols,
|
|
413
|
+
MaybeFn,
|
|
414
|
+
} from "../../types";
|
|
415
|
+
import { humaniseErrorKey } from "../../utils/formErrorTargets";
|
|
405
416
|
import { getByPath, setByPath } from "../../utils/objectPath";
|
|
406
417
|
|
|
407
418
|
// Async to break the DXField <-> DXRepeater circular import.
|
|
@@ -615,6 +626,21 @@ function resolveMaybe<TValue>(
|
|
|
615
626
|
return value;
|
|
616
627
|
}
|
|
617
628
|
|
|
629
|
+
/** Resolve display TEXT (label, hint, info, switch text) the way
|
|
630
|
+
* resolveMaybe does, but a function that throws (a repeater row missing a
|
|
631
|
+
* property it reads) gives `undefined` instead of breaking the render (#204).
|
|
632
|
+
* Behaviour options (disabled, readonly, plaintext) stay unguarded: no
|
|
633
|
+
* fallback for those is safe to guess. */
|
|
634
|
+
function resolveText(
|
|
635
|
+
value: MaybeFn<string> | undefined,
|
|
636
|
+
): string | undefined {
|
|
637
|
+
try {
|
|
638
|
+
return resolveMaybe(value);
|
|
639
|
+
} catch {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
618
644
|
const fieldValue = computed({
|
|
619
645
|
get: () =>
|
|
620
646
|
usePathSemantics.value
|
|
@@ -687,13 +713,19 @@ function setValue(value: any): void {
|
|
|
687
713
|
|
|
688
714
|
const fieldState = computed(() => props.form.getState(errorKey.value));
|
|
689
715
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
716
|
+
// A label function that throws falls back to the humanised key, as the
|
|
717
|
+
// repeater's headers and the error summary do; a missing label still shows
|
|
718
|
+
// the raw key, as it always has.
|
|
719
|
+
const resolvedLabel = computed(() => {
|
|
720
|
+
if (typeof props.field.label !== "function") {
|
|
721
|
+
return props.field.label ?? props.field.key;
|
|
722
|
+
}
|
|
723
|
+
return resolveText(props.field.label) ?? humaniseErrorKey(props.field.key);
|
|
724
|
+
});
|
|
693
725
|
|
|
694
|
-
const resolvedHint = computed(() =>
|
|
726
|
+
const resolvedHint = computed(() => resolveText(props.field.hint));
|
|
695
727
|
|
|
696
|
-
const resolvedInfo = computed(() =>
|
|
728
|
+
const resolvedInfo = computed(() => resolveText(props.field.info));
|
|
697
729
|
|
|
698
730
|
// ————————————————— switch-list (#160)
|
|
699
731
|
|
|
@@ -759,8 +791,8 @@ const switchModel = computed({
|
|
|
759
791
|
*/
|
|
760
792
|
const switchText = computed(() => {
|
|
761
793
|
const contextual = switchIsOn.value
|
|
762
|
-
?
|
|
763
|
-
:
|
|
794
|
+
? resolveText(props.field.textWhenTrue)
|
|
795
|
+
: resolveText(props.field.textWhenFalse);
|
|
764
796
|
return contextual ?? resolvedLabel.value;
|
|
765
797
|
});
|
|
766
798
|
|
|
@@ -780,12 +812,6 @@ const isPlaintext = computed(() => resolveMaybe(props.field.plaintext) ?? false)
|
|
|
780
812
|
|
|
781
813
|
const passwordRevealed = ref(false);
|
|
782
814
|
|
|
783
|
-
// See the template: the searchable select can't render before its options
|
|
784
|
-
// resolve, or it shows the raw id instead of the option's label.
|
|
785
|
-
const searchableOptionsReady = computed(
|
|
786
|
-
() => !props.field.optionsLoader || loadedOptions.value !== null,
|
|
787
|
-
);
|
|
788
|
-
|
|
789
815
|
const isRevealablePassword = computed(
|
|
790
816
|
() =>
|
|
791
817
|
props.field.type === "password" &&
|
|
@@ -856,7 +882,15 @@ const datalistId = useId();
|
|
|
856
882
|
|
|
857
883
|
// ————————————————— async options
|
|
858
884
|
|
|
859
|
-
const { loadedOptions } = useAsyncOptions(
|
|
885
|
+
const { loadedOptions, firstLoadSettled } = useAsyncOptions(
|
|
886
|
+
() => props.field,
|
|
887
|
+
effectiveModel,
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
// The first load has not settled: nothing to label the value with yet. Once
|
|
891
|
+
// it has (even by failing, which leaves the static options), a reload never
|
|
892
|
+
// makes the field pending again: it keeps what it already shows.
|
|
893
|
+
const optionsPending = computed(() => !firstLoadSettled.value);
|
|
860
894
|
|
|
861
895
|
const resolvedOptions = computed<FieldOption[] | undefined>(
|
|
862
896
|
() => loadedOptions.value ?? props.field.options,
|