@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.
Files changed (40) hide show
  1. package/dist/components/base/DFormSelect.vue.d.ts +1 -1
  2. package/dist/components/extended/DXChoiceField.vue.d.ts +3 -3
  3. package/dist/components/extended/DXForm.vue.d.ts +66 -1
  4. package/dist/components/extended/DXFormErrorSummary.vue.d.ts +70 -0
  5. package/dist/components/extended/DXFormField.vue.d.ts +3 -0
  6. package/dist/components/extended/DXLoading.vue.d.ts +3 -0
  7. package/dist/components/extended/DXTable.vue.d.ts +20 -0
  8. package/dist/components/extended/DXTableEditorModal.vue.d.ts +14 -10
  9. package/dist/composables/useAsyncOptions.d.ts +14 -1
  10. package/dist/composables/useForm.d.ts +26 -0
  11. package/dist/dashboard-for-laravel.js +13605 -13058
  12. package/dist/dashboard-for-laravel.js.map +1 -1
  13. package/dist/dashboard-for-laravel.umd.cjs +8 -8
  14. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  15. package/dist/index.d.ts +5 -1
  16. package/dist/style.css +1 -1
  17. package/dist/types/index.d.ts +12 -0
  18. package/dist/utils/formErrorTargets.d.ts +86 -0
  19. package/docs/public/api-reference.json +148 -8
  20. package/docs/public/docs-map.md +3 -2
  21. package/docs/public/llms.txt +3 -2
  22. package/package.json +9 -8
  23. package/resources/css/theme.scss +176 -0
  24. package/resources/js/components/base/DFormSelect.vue +5 -2
  25. package/resources/js/components/extended/DXChoiceField.vue +42 -26
  26. package/resources/js/components/extended/DXField.vue +51 -17
  27. package/resources/js/components/extended/DXForm.vue +676 -28
  28. package/resources/js/components/extended/DXFormErrorSummary.vue +232 -0
  29. package/resources/js/components/extended/DXFormField.vue +11 -2
  30. package/resources/js/components/extended/DXLoading.vue +18 -11
  31. package/resources/js/components/extended/DXRepeater.vue +30 -4
  32. package/resources/js/components/extended/DXTable.vue +38 -9
  33. package/resources/js/components/extended/DXTableEditorModal.vue +29 -1
  34. package/resources/js/composables/useAsyncOptions.ts +30 -5
  35. package/resources/js/composables/useForm.ts +196 -18
  36. package/resources/js/composables/useResourceEditor.ts +140 -107
  37. package/resources/js/index.ts +15 -0
  38. package/resources/js/types/index.ts +13 -0
  39. package/resources/js/utils/formErrorTargets.ts +317 -0
  40. 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
- <template v-if="field.type === 'select' && field.searchable">
18
- <DAutocomplete
19
- v-if="optionsReady"
20
- :model-value="modelValue"
21
- :options="options"
22
- :placeholder="field.placeholder"
23
- :required="field.required"
24
- :state="state"
25
- :disabled="disabled"
26
- open-on-focus
27
- v-bind="controlProps"
28
- @update:model-value="emit('update:modelValue', $event)"
29
- />
30
- <!-- BAutocomplete resolves the input's display text from the model AT
31
- MOUNT and doesn't re-derive it when `options` arrive later, so an
32
- async list would leave the raw id showing ("51" instead of
33
- "Waitrose") — and a long list is exactly the case that loads async.
34
- Hold the control until the options are in. -->
35
- <DFormInput v-else placeholder="Loading…" disabled />
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
- * Whether async options have resolved (a searchable select must not render
104
- * before its options load, or it shows the raw id instead of the label).
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
- optionsReady?: boolean;
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
- <div v-if="field.span" :class="field.class || 'mb-3'">
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-ready="searchableOptionsReady"
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 { FieldDefinition, FieldOption, FieldType, LabelCols } from "../../types";
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
- const resolvedLabel = computed(
691
- () => resolveMaybe(props.field.label) ?? props.field.key,
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(() => resolveMaybe(props.field.hint));
726
+ const resolvedHint = computed(() => resolveText(props.field.hint));
695
727
 
696
- const resolvedInfo = computed(() => resolveMaybe(props.field.info));
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
- ? resolveMaybe(props.field.textWhenTrue)
763
- : resolveMaybe(props.field.textWhenFalse);
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(() => props.field, effectiveModel);
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,