nuxt-ui-tools 1.3.0 → 1.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.
Files changed (94) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +10 -2
  3. package/dist/runtime/dashboard/composables/use-dashboard-remote-options.js +8 -3
  4. package/dist/runtime/dashboard/types/filters.d.ts +7 -1
  5. package/dist/runtime/dashboard/utils/builders/dashboard-filters.js +19 -2
  6. package/dist/runtime/form/components/actions/form-actions.vue +13 -127
  7. package/dist/runtime/form/components/page/form-page-actions.d.vue.ts +3 -0
  8. package/dist/runtime/form/components/page/form-page-actions.vue +40 -0
  9. package/dist/runtime/form/components/page/form-page-actions.vue.d.ts +3 -0
  10. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +24 -0
  11. package/dist/runtime/form/components/page/form-page-header.vue +99 -0
  12. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +24 -0
  13. package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +34 -0
  14. package/dist/runtime/form/components/page/form-page-navigation.vue +229 -0
  15. package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +34 -0
  16. package/dist/runtime/form/components/page/form-page-section.d.vue.ts +19 -0
  17. package/dist/runtime/form/components/page/form-page-section.vue +114 -0
  18. package/dist/runtime/form/components/page/form-page-section.vue.d.ts +19 -0
  19. package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +21 -0
  20. package/dist/runtime/form/components/page/form-page-sections.vue +75 -0
  21. package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +21 -0
  22. package/dist/runtime/form/components/page/form-page.d.vue.ts +80 -0
  23. package/dist/runtime/form/components/page/form-page.vue +91 -0
  24. package/dist/runtime/form/components/page/form-page.vue.d.ts +80 -0
  25. package/dist/runtime/form/components/renderer/form-field-shell.vue +7 -11
  26. package/dist/runtime/form/components/root/form.vue +25 -194
  27. package/dist/runtime/form/composables/use-field-options.js +19 -0
  28. package/dist/runtime/form/composables/use-form-action-buttons.d.ts +32 -0
  29. package/dist/runtime/form/composables/use-form-action-buttons.js +89 -0
  30. package/dist/runtime/form/composables/use-form-actions.d.ts +8 -5
  31. package/dist/runtime/form/composables/use-form-actions.js +1 -1
  32. package/dist/runtime/form/composables/use-form-page-scroll.d.ts +22 -0
  33. package/dist/runtime/form/composables/use-form-page-scroll.js +178 -0
  34. package/dist/runtime/form/composables/use-form-page.d.ts +114 -0
  35. package/dist/runtime/form/composables/use-form-page.js +83 -0
  36. package/dist/runtime/form/composables/use-form-root.d.ts +34 -0
  37. package/dist/runtime/form/composables/use-form-root.js +171 -0
  38. package/dist/runtime/form/composables/use-form-runtime.js +13 -4
  39. package/dist/runtime/form/composables/use-form.js +2 -22
  40. package/dist/runtime/form/composables/use-remote-field-options.js +21 -2
  41. package/dist/runtime/form/fields/checkbox-card/component.vue +28 -19
  42. package/dist/runtime/form/fields/checkbox-card/types.d.ts +2 -5
  43. package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +17 -0
  44. package/dist/runtime/form/fields/choice-card/choice-card-label.vue +50 -0
  45. package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +17 -0
  46. package/dist/runtime/form/fields/choice-card/types.d.ts +31 -0
  47. package/dist/runtime/form/fields/choice-card/types.js +0 -0
  48. package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +22 -0
  49. package/dist/runtime/form/fields/choice-card/use-choice-card.js +52 -0
  50. package/dist/runtime/form/fields/radio-card/component.vue +17 -18
  51. package/dist/runtime/form/fields/radio-card/types.d.ts +2 -4
  52. package/dist/runtime/form/index.d.ts +1 -1
  53. package/dist/runtime/form/index.js +7 -1
  54. package/dist/runtime/form/schema/index.d.ts +1 -1
  55. package/dist/runtime/form/schema/index.js +1 -0
  56. package/dist/runtime/form/schema/page.d.ts +50 -0
  57. package/dist/runtime/form/schema/page.js +25 -0
  58. package/dist/runtime/form/types/index.d.ts +1 -0
  59. package/dist/runtime/form/types/options.d.ts +22 -2
  60. package/dist/runtime/form/types/page.d.ts +172 -0
  61. package/dist/runtime/form/types/page.js +0 -0
  62. package/dist/runtime/form/types/ui.d.ts +60 -0
  63. package/dist/runtime/form/utils/controls.d.ts +15 -0
  64. package/dist/runtime/form/utils/controls.js +38 -0
  65. package/dist/runtime/form/utils/layout.js +3 -1
  66. package/dist/runtime/form/utils/page.d.ts +20 -0
  67. package/dist/runtime/form/utils/page.js +107 -0
  68. package/dist/runtime/form/utils/state.d.ts +2 -0
  69. package/dist/runtime/form/utils/state.js +1 -1
  70. package/dist/runtime/form/utils/ui.js +1 -0
  71. package/dist/runtime/i18n/locales/en.js +18 -0
  72. package/dist/runtime/i18n/locales/fr.js +18 -0
  73. package/dist/runtime/i18n/types.d.ts +29 -0
  74. package/dist/runtime/shared/index.d.ts +1 -0
  75. package/dist/runtime/shared/index.js +1 -0
  76. package/dist/runtime/shared/types/query.d.ts +5 -1
  77. package/dist/runtime/shared/types/remote-options.d.ts +56 -0
  78. package/dist/runtime/shared/utils/define-remote-options.d.ts +23 -0
  79. package/dist/runtime/shared/utils/define-remote-options.js +24 -0
  80. package/dist/runtime/shared/utils/remote-options-query.d.ts +6 -0
  81. package/dist/runtime/shared/utils/remote-options-query.js +18 -0
  82. package/dist/runtime/table/components/data-list/data-list-filter-tags.d.vue.ts +1 -1
  83. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue +1 -1
  84. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue.d.ts +1 -1
  85. package/dist/runtime/table/components/filters/panel/filter-panel.vue +14 -2
  86. package/dist/runtime/table/components/filters/shared/filter-option-multiple-list.vue +2 -2
  87. package/dist/runtime/table/components/filters/shared/filter-option-single-list.vue +2 -2
  88. package/dist/runtime/table/components/filters/shared/filter-option-tree-content.vue +5 -5
  89. package/dist/runtime/table/components/filters/shared/filter-trigger-tag.vue +7 -1
  90. package/dist/runtime/table/composables/use-table-filter-selected-options.js +16 -1
  91. package/dist/runtime/table/types/filters.d.ts +7 -0
  92. package/dist/runtime/table/types/remote-options.d.ts +3 -16
  93. package/dist/runtime/table/utils/remote-table-options.js +37 -37
  94. package/package.json +1 -1
@@ -0,0 +1,91 @@
1
+ <script setup>
2
+ import { computed, useTemplateRef } from "vue";
3
+ import { provideFormPage, useFormPage } from "../../composables/use-form-page";
4
+ import { useFormRoot } from "../../composables/use-form-root";
5
+ import { mergeFormUiClass } from "../../utils/ui";
6
+ import FormPageHeader from "./form-page-header.vue";
7
+ import FormPageNavigation from "./form-page-navigation.vue";
8
+ import FormPageSections from "./form-page-sections.vue";
9
+ const {
10
+ form,
11
+ hash = true,
12
+ ui = void 0
13
+ } = defineProps({
14
+ form: { type: Object, required: true },
15
+ hash: { type: Boolean, required: false },
16
+ ui: { type: Object, required: false }
17
+ });
18
+ const emit = defineEmits(["submit", "cancel"]);
19
+ defineSlots();
20
+ const element = useTemplateRef("element");
21
+ const root = useFormRoot({
22
+ form: () => form,
23
+ input: () => void 0,
24
+ onCancelled: (value) => emit("cancel", value),
25
+ onSubmitted: (value, result) => emit("submit", value, result),
26
+ schema: () => void 0,
27
+ syncInput: () => void 0,
28
+ ui: () => ui,
29
+ validate: () => void 0
30
+ });
31
+ const page = provideFormPage(useFormPage({ element: () => element.value, hash: () => hash, root }));
32
+ const pageUi = computed(() => root.formUi.ui.value.page?.ui);
33
+ </script>
34
+
35
+ <template>
36
+ <form
37
+ ref="element"
38
+ novalidate
39
+ :class="
40
+ mergeFormUiClass('@container/form-page h-full min-w-0 overflow-y-auto bg-muted', pageUi?.root)
41
+ "
42
+ :style="{ '--nut-form-page-header': `${page.stickyOffset.value}px` }"
43
+ data-form-page
44
+ @submit.prevent="root.submit"
45
+ >
46
+ <slot>
47
+ <FormPageHeader>
48
+ <template v-if="$slots['header-leading']" #leading>
49
+ <slot name="header-leading" />
50
+ </template>
51
+ <template v-if="$slots['header-eyebrow']" #eyebrow>
52
+ <slot name="header-eyebrow" />
53
+ </template>
54
+ <template v-if="$slots['header-title']" #title>
55
+ <slot name="header-title" />
56
+ </template>
57
+ <template v-if="$slots['header-description']" #description>
58
+ <slot name="header-description" />
59
+ </template>
60
+ <template v-if="$slots['header-actions']" #actions>
61
+ <slot name="header-actions" />
62
+ </template>
63
+ </FormPageHeader>
64
+ <div
65
+ :class="
66
+ mergeFormUiClass(
67
+ 'grid gap-4 px-4 pt-4 pb-16 @3xl/form-page:grid-cols-[200px_minmax(0,1fr)] @3xl/form-page:items-start @3xl/form-page:gap-7 @3xl/form-page:px-7 @3xl/form-page:pt-6 @3xl/form-page:pb-20 @5xl/form-page:grid-cols-[230px_minmax(0,760px)]',
68
+ pageUi?.body
69
+ )
70
+ "
71
+ >
72
+ <FormPageNavigation>
73
+ <template v-if="$slots['navigation-title']" #title>
74
+ <slot name="navigation-title" />
75
+ </template>
76
+ <template v-if="$slots['navigation-item']" #item="slotProps">
77
+ <slot name="navigation-item" v-bind="slotProps" />
78
+ </template>
79
+ <template v-if="$slots['navigation-footer']" #footer="slotProps">
80
+ <slot name="navigation-footer" v-bind="slotProps" />
81
+ </template>
82
+ </FormPageNavigation>
83
+ <FormPageSections>
84
+ <template v-if="$slots['section-actions']" #actions="slotProps">
85
+ <slot name="section-actions" v-bind="slotProps" />
86
+ </template>
87
+ </FormPageSections>
88
+ </div>
89
+ </slot>
90
+ </form>
91
+ </template>
@@ -0,0 +1,80 @@
1
+ import type { FormPageSectionState, FormRendererController, FormUiConfig, FormValue } from '../../types/index.js';
2
+ /**
3
+ * A form laid out as a page: a header with the title and the actions, a navigation that follows
4
+ * the section in view, and one card per section. Render a schema from `defineFormPageSchema`.
5
+ *
6
+ * The page scrolls on its own, so give it a height (`h-full` in a layout that sizes its main
7
+ * area). Put parts in the default slot to compose another layout: `FormPageHeader`,
8
+ * `FormPageNavigation`, `FormPageSections`, and `FormPageActions` read the page they render in.
9
+ *
10
+ * @example
11
+ * ```vue
12
+ * <!-- const form = useForm({ schema: accountFormSchema(), onSubmit: createAccount }) -->
13
+ * <UiFormPage :form />
14
+ *
15
+ * <UiFormPage :form>
16
+ * <UiFormPageHeader />
17
+ * <div class="grid grid-cols-[minmax(0,1fr)_240px] gap-6 p-6">
18
+ * <UiFormPageSections />
19
+ * <UiFormPageNavigation />
20
+ * </div>
21
+ * </UiFormPage>
22
+ * ```
23
+ */
24
+ type __VLS_Props = {
25
+ form: FormRendererController;
26
+ /**
27
+ * Records the section picked in the navigation in the URL hash (`#identity`), and opens the
28
+ * page on the section the hash names. Defaults to `true`.
29
+ */
30
+ hash?: boolean;
31
+ /** Presentation overrides, merged after the app config and the schema `ui`. */
32
+ ui?: FormUiConfig;
33
+ };
34
+ type __VLS_Slots = {
35
+ /** Replaces the whole layout with parts of the page. */
36
+ default?: () => unknown;
37
+ /** Slots of the default header: see `FormPageHeader`. */
38
+ 'header-leading'?: () => unknown;
39
+ 'header-eyebrow'?: () => unknown;
40
+ 'header-title'?: () => unknown;
41
+ 'header-description'?: () => unknown;
42
+ 'header-actions'?: () => unknown;
43
+ /** Slots of the default navigation: see `FormPageNavigation`. */
44
+ 'navigation-title'?: () => unknown;
45
+ 'navigation-item'?: (props: {
46
+ section: FormPageSectionState;
47
+ active: boolean;
48
+ select: () => void;
49
+ }) => unknown;
50
+ 'navigation-footer'?: (props: {
51
+ sections: readonly FormPageSectionState[];
52
+ remaining: number;
53
+ modified: number;
54
+ }) => unknown;
55
+ /** Extra buttons in a section header: see `FormPageSections`. */
56
+ 'section-actions'?: (props: {
57
+ section: FormPageSectionState;
58
+ }) => unknown;
59
+ };
60
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
61
+ submit: (value: import("../../../shared/index.js").GenericObject, result: {
62
+ success: boolean;
63
+ data?: FormValue;
64
+ }) => any;
65
+ cancel: (value: import("../../../shared/index.js").GenericObject) => any;
66
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
67
+ onSubmit?: ((value: import("../../../shared/index.js").GenericObject, result: {
68
+ success: boolean;
69
+ data?: FormValue;
70
+ }) => any) | undefined;
71
+ onCancel?: ((value: import("../../../shared/index.js").GenericObject) => any) | undefined;
72
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
73
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
74
+ declare const _default: typeof __VLS_export;
75
+ export default _default;
76
+ type __VLS_WithSlots<T, S> = T & {
77
+ new (): {
78
+ $slots: S;
79
+ };
80
+ };
@@ -10,8 +10,8 @@ import { useUiToolsLocale } from "../../../i18n/use-locale";
10
10
  import { useFormFieldBare } from "../../composables/use-form-field-chrome";
11
11
  import { useFormRuntimeContext } from "../../composables/use-form-runtime";
12
12
  import { useFormUi } from "../../composables/use-form-ui";
13
+ import { getSchemaDirtyCheck } from "../../utils/controls";
13
14
  import { createFormFieldInstance } from "../../utils/field-instance";
14
- import { isRecord } from "../../utils/path";
15
15
  import { isFunction, isNumber, isString } from "../../utils/predicate";
16
16
  import { resolveRequired } from "../../utils/state";
17
17
  import { resolveFieldDescription, resolveFormText } from "../../utils/text";
@@ -85,21 +85,17 @@ const help = computed(
85
85
  );
86
86
  const shellHelp = computed(() => props.inlineLabel ? void 0 : help.value);
87
87
  const dirty = computed(
88
- () => "dirtyCheck" in props.field && (props.field.dirtyCheck === true || schemaDirtyCheck()) && form.dirtyPaths.value.includes(props.path.join("."))
88
+ () => "dirtyCheck" in props.field && (props.field.dirtyCheck === true || getSchemaDirtyCheck(form.schema.value)) && form.dirtyPaths.value.includes(props.path.join("."))
89
89
  );
90
- function schemaDirtyCheck() {
91
- const schema = form.schema.value;
92
- if (!isRecord(schema) || !isRecord(schema.controls)) {
93
- return false;
94
- }
95
- return schema.controls.dirtyCheck === true;
96
- }
97
90
  const open = ref(!("collapsed" in props.field && props.field.collapsed === true));
98
91
  const collapsible = computed(() => "collapsible" in props.field && props.field.collapsible === true);
99
92
  const fieldUi = computed(() => formUi.ui.value.field?.ui);
93
+ const stacked = computed(
94
+ () => orientation.value === "vertical" && Boolean(shellLabel.value || shellDescription.value)
95
+ );
100
96
  const nuxtFieldUi = computed(() => ({
101
97
  container: mergeFormUiClass(
102
- orientation.value === "horizontal" ? "min-w-0 flex-1" : void 0,
98
+ orientation.value === "horizontal" ? "min-w-0 flex-1" : stacked.value ? "mt-0" : void 0,
103
99
  fieldUi.value?.container
104
100
  ),
105
101
  description: fieldUi.value?.description,
@@ -111,7 +107,7 @@ const nuxtFieldUi = computed(() => ({
111
107
  orientation.value === "horizontal" && labelWidth.value ? "w-(--nut-form-label-width) shrink-0" : void 0,
112
108
  fieldUi.value?.labelWrapper
113
109
  ),
114
- root: fieldUi.value?.root,
110
+ root: mergeFormUiClass(stacked.value ? "flex flex-col gap-1.5" : void 0, fieldUi.value?.root),
115
111
  wrapper: fieldUi.value?.wrapper
116
112
  }));
117
113
  function resetField() {
@@ -4,27 +4,17 @@ import UBadge from "@nuxt/ui/components/Badge.vue";
4
4
  import UButton from "@nuxt/ui/components/Button.vue";
5
5
  import USkeleton from "@nuxt/ui/components/Skeleton.vue";
6
6
  import { useScrollShadow } from "@nuxt/ui/composables/useScrollShadow";
7
- import { useAppConfig } from "nuxt/app";
8
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
9
- import { useRouter } from "vue-router";
7
+ import { computed, nextTick, ref, watch } from "vue";
10
8
  import { useUiToolsLocale } from "#ui-tools/i18n";
11
9
  import { useFormActions } from "../../composables/use-form-actions";
12
10
  import { useFormGridLayout } from "../../composables/use-form-layout";
13
- import { provideFormRuntime, useFormRuntime } from "../../composables/use-form-runtime";
14
- import { provideFormUi } from "../../composables/use-form-ui";
11
+ import { useFormRoot } from "../../composables/use-form-root";
15
12
  import { getFormHeader } from "../../utils/overlay";
16
13
  import { isRecord } from "../../utils/path";
17
- import {
18
- isBoolean,
19
- isFunction,
20
- isNumber,
21
- isString,
22
- isUndefined,
23
- stringArray
24
- } from "../../utils/predicate";
14
+ import { isFunction, isNumber, isString } from "../../utils/predicate";
25
15
  import { getSchemaFields, getSchemaSteps, isSteppedSchema } from "../../utils/state";
26
16
  import { resolveFormText } from "../../utils/text";
27
- import { mergeFormUi, mergeFormUiClass, resolveAppFormUi } from "../../utils/ui";
17
+ import { mergeFormUiClass } from "../../utils/ui";
28
18
  import FormActions from "../actions/form-actions.vue";
29
19
  import FormFieldRenderer from "../renderer/form-field-renderer.vue";
30
20
  import FormDirectionalTransition from "../utils/form-directional-transition.vue";
@@ -38,68 +28,28 @@ const props = defineProps({
38
28
  showCloseButton: { type: Boolean, required: false },
39
29
  ui: { type: Object, required: false }
40
30
  });
41
- const router = useRouter();
42
31
  const emit = defineEmits(["submit", "cancel"]);
43
32
  const { t } = useUiToolsLocale();
44
- const schemaRef = computed(() => props.form?.schema.value ?? props.schema ?? {});
45
- const appConfig = useAppConfig();
46
- const formUi = provideFormUi(
47
- computed(
48
- () => mergeFormUi(resolveAppFormUi(appConfig), getSchemaUi(schemaRef.value), props.ui)
49
- )
50
- );
51
- const inputRef = computed(() => props.form?.input.value ?? props.input);
52
- const syncInputRef = computed(
53
- () => props.form?.syncInput.value ?? props.syncInput ?? getSchemaSyncInput(schemaRef.value)
54
- );
55
- const validationModeRef = computed(
56
- () => props.form?.validationMode.value ?? props.validate ?? getSchemaValidationMode(schemaRef.value)
57
- );
58
- const runtime = useFormRuntime({
59
- input: inputRef,
33
+ const {
34
+ cancel,
35
+ contextError,
36
+ contextLoading,
37
+ contextPending,
38
+ formUi,
39
+ refreshContext,
40
+ runtime,
60
41
  schema: schemaRef,
61
- syncInput: syncInputRef,
62
- validationMode: validationModeRef
42
+ submit
43
+ } = useFormRoot({
44
+ form: () => props.form,
45
+ input: () => props.input,
46
+ onCancelled: (value) => emit("cancel", value),
47
+ onSubmitted: (value, result) => emit("submit", value, result),
48
+ schema: () => props.schema,
49
+ syncInput: () => props.syncInput,
50
+ ui: () => props.ui,
51
+ validate: () => props.validate
63
52
  });
64
- provideFormRuntime(runtime);
65
- watch(
66
- () => props.form,
67
- (controller, previousController) => {
68
- previousController?.unbind(runtime);
69
- controller?.bind(runtime);
70
- },
71
- { immediate: true }
72
- );
73
- onBeforeUnmount(() => props.form?.unbind(runtime));
74
- onMounted(async () => {
75
- await nextTick();
76
- const target = getSchemaAutoFocus(schemaRef.value);
77
- if (isString(target)) {
78
- await runtime.focusField(target);
79
- return;
80
- }
81
- if (target === true) {
82
- await focusFirstRenderedField();
83
- }
84
- });
85
- if (import.meta.client) {
86
- let handleBeforeUnload = function(event) {
87
- if (!shouldConfirmDirtyNavigation()) {
88
- return;
89
- }
90
- event.preventDefault();
91
- event.returnValue = "";
92
- };
93
- window.addEventListener("beforeunload", handleBeforeUnload);
94
- onBeforeUnmount(() => window.removeEventListener("beforeunload", handleBeforeUnload));
95
- }
96
- const removeRouteGuard = router.beforeEach(() => {
97
- if (!shouldConfirmDirtyNavigation()) {
98
- return true;
99
- }
100
- return window.confirm(getDirtyNavigationMessage());
101
- });
102
- onBeforeUnmount(removeRouteGuard);
103
53
  const viewportRef = ref(null);
104
54
  const viewportShadow = useScrollShadow(viewportRef, { size: 20 });
105
55
  const displayedStepIndex = ref(runtime.currentStepIndex.value);
@@ -114,9 +64,6 @@ watch(
114
64
  });
115
65
  }
116
66
  );
117
- const parentPath = computed(
118
- () => runtime.currentStepRoot.value ? [runtime.currentStepRoot.value] : []
119
- );
120
67
  const displayedStep = computed(
121
68
  () => isSteppedSchema(schemaRef.value) ? getSchemaSteps(schemaRef.value)[displayedStepIndex.value] : void 0
122
69
  );
@@ -167,56 +114,10 @@ const footerClass = computed(
167
114
  formUi.ui.value.root?.ui?.footer
168
115
  )
169
116
  );
170
- const actions = useFormActions({ runtime, shell });
171
- const contextPending = computed(
172
- () => Object.values(runtime.context).some(
173
- (resource) => "pending" in resource && resource.pending && isUndefined(resource.value)
174
- )
175
- );
176
- const contextLoading = computed(
177
- () => Object.values(runtime.context).some((resource) => "loading" in resource && resource.loading)
178
- );
179
- const contextError = computed(() => {
180
- for (const resource of Object.values(runtime.context)) {
181
- if (!("error" in resource) || !resource.error) {
182
- continue;
183
- }
184
- if (resource.error instanceof Error) {
185
- return resource.error.message;
186
- }
187
- if (isString(resource.error)) {
188
- return resource.error;
189
- }
190
- return t("form.states.contextError.description");
191
- }
192
- return void 0;
117
+ const actions = useFormActions({
118
+ runtime,
119
+ slot: () => shell.value === "inline" ? "left" : "right"
193
120
  });
194
- async function submit() {
195
- if (props.form) {
196
- const result2 = await props.form.submitHandler();
197
- if (result2.success) {
198
- emit("submit", runtime.output.value, result2);
199
- }
200
- return;
201
- }
202
- const result = await runtime.submitHandler();
203
- if (result.success) {
204
- emit("submit", runtime.output.value, result);
205
- }
206
- }
207
- async function refreshContext() {
208
- await Promise.all(
209
- Object.values(runtime.context).flatMap(
210
- (resource) => "refresh" in resource ? [resource.refresh()] : []
211
- )
212
- );
213
- }
214
- function cancel() {
215
- if (shouldConfirmDirtyNavigation() && !window.confirm(getDirtyNavigationMessage())) {
216
- return;
217
- }
218
- emit("cancel", runtime.output.value);
219
- }
220
121
  defineExpose({
221
122
  actionPending: runtime.actionPending,
222
123
  dirtyPaths: runtime.dirtyPaths,
@@ -268,13 +169,6 @@ function headerDisplayed(display, currentShell) {
268
169
  function isRenderShell(value) {
269
170
  return value === "inline" || value === "drawer" || value === "modal" || value === "fullscreen";
270
171
  }
271
- function getSchemaUi(schema) {
272
- if (!isRecord(schema)) {
273
- return void 0;
274
- }
275
- const value = Object.getOwnPropertyDescriptor(schema, "ui")?.value;
276
- return isRecord(value) ? value : void 0;
277
- }
278
172
  function getSchemaShowStepper(schema) {
279
173
  if (!isRecord(schema)) {
280
174
  return true;
@@ -282,69 +176,6 @@ function getSchemaShowStepper(schema) {
282
176
  const value = Object.getOwnPropertyDescriptor(schema, "showStepper")?.value;
283
177
  return value !== false;
284
178
  }
285
- function getSchemaSyncInput(schema) {
286
- const controls = getSchemaControls(schema);
287
- const value = controls ? Object.getOwnPropertyDescriptor(controls, "syncInput")?.value : void 0;
288
- if (isBoolean(value)) {
289
- return value;
290
- }
291
- return stringArray(value);
292
- }
293
- function getSchemaValidationMode(schema) {
294
- const controls = getSchemaControls(schema);
295
- const value = controls ? Object.getOwnPropertyDescriptor(controls, "validate")?.value : void 0;
296
- return value === false || value === "required" || value === "validators" ? value : true;
297
- }
298
- function getSchemaControls(schema) {
299
- if (!isRecord(schema)) {
300
- return;
301
- }
302
- const controls = Object.getOwnPropertyDescriptor(schema, "controls")?.value;
303
- return isRecord(controls) ? controls : void 0;
304
- }
305
- function getSchemaAutoFocus(schema) {
306
- const controls = getSchemaControls(schema);
307
- const value = controls ? Object.getOwnPropertyDescriptor(controls, "autoFocus")?.value : void 0;
308
- return isString(value) || isBoolean(value) ? value : false;
309
- }
310
- function dirtyNavigationConfig() {
311
- const controls = getSchemaControls(schemaRef.value);
312
- return controls ? Object.getOwnPropertyDescriptor(controls, "confirmNavOnDirty")?.value : void 0;
313
- }
314
- function shouldConfirmDirtyNavigation() {
315
- if (!runtime.isDirty.value) {
316
- return false;
317
- }
318
- const config = dirtyNavigationConfig();
319
- if (!config) {
320
- return false;
321
- }
322
- if (!isRecord(config)) {
323
- return config === true;
324
- }
325
- const ignored = stringArray(config.ignorePaths);
326
- return runtime.dirtyPaths.value.some(
327
- (path) => !ignored.some((ignoredPath) => path === ignoredPath || path.startsWith(`${ignoredPath}.`))
328
- );
329
- }
330
- function getDirtyNavigationMessage() {
331
- const config = dirtyNavigationConfig();
332
- if (!isRecord(config)) {
333
- return "You have unsaved changes. Close this form?";
334
- }
335
- const message = Object.getOwnPropertyDescriptor(config, "message")?.value;
336
- return resolveFormText(message) ?? "You have unsaved changes. Close this form?";
337
- }
338
- async function focusFirstRenderedField() {
339
- const paths = runtime.currentFields.value.map(
340
- (field) => `${parentPath.value.join(".")}${parentPath.value.length ? "." : ""}${field.key}`
341
- );
342
- for (const path of paths) {
343
- if (await runtime.focusField(path)) {
344
- return;
345
- }
346
- }
347
- }
348
179
  </script>
349
180
 
350
181
  <template>
@@ -350,8 +350,27 @@ function resolveRemoteOptionConfig(field) {
350
350
  return null;
351
351
  }
352
352
  const options = Object.getOwnPropertyDescriptor(field, "options")?.value;
353
+ if (isRemoteLoaderOptionConfig(options)) {
354
+ const { loader, ...fieldOptions } = options;
355
+ return {
356
+ ...fieldOptions,
357
+ pagination: loader.pagination,
358
+ queryKeyFor: loader.queryKeyFor,
359
+ resolveSelected: loader.resolveSelected,
360
+ search: loader.search,
361
+ selectedQueryKeyFor: loader.selectedQueryKeyFor,
362
+ source: loader.load
363
+ };
364
+ }
353
365
  return isRemoteOptionConfig(options) ? options : null;
354
366
  }
367
+ function isRemoteLoaderOptionConfig(value) {
368
+ if (!isRecord(value) || value.mode !== "remote" || !isRecord(value.loader)) {
369
+ return false;
370
+ }
371
+ const { loader } = value;
372
+ return isFunction(loader.load) && isFunction(loader.resolveSelected) && isRecord(loader.pagination);
373
+ }
355
374
  function isRemoteOptionConfig(value) {
356
375
  return isRecord(value) && value.mode === "remote" && isFunction(Object.getOwnPropertyDescriptor(value, "source")?.value) && isRecord(value.pagination);
357
376
  }
@@ -0,0 +1,32 @@
1
+ import type { FormAction, FormRuntime } from '../types/index.js';
2
+ /**
3
+ * Turns form actions into Nuxt UI buttons: visibility, disabled and loading states, links, and
4
+ * what a click runs. The form footer and the form page actions render from it.
5
+ */
6
+ export declare function useFormActionButtons(params: {
7
+ runtime: FormRuntime;
8
+ actions: () => readonly FormAction[];
9
+ onCancel: () => void;
10
+ }): {
11
+ button: (action: FormAction) => {
12
+ readonly color: import("../types/index.js").FormActionColor | undefined;
13
+ readonly disabled: boolean;
14
+ readonly icon: string | undefined;
15
+ readonly label: string | undefined;
16
+ readonly loading: boolean;
17
+ readonly size: "xs" | "sm" | "md" | "lg" | "xl";
18
+ readonly to: string | undefined;
19
+ readonly trailingIcon: string | undefined;
20
+ readonly type: "button" | "submit";
21
+ readonly variant: import("../types/index.js").FormActionVariant | undefined;
22
+ };
23
+ fills: typeof fills;
24
+ run: (action: FormAction) => Promise<void>;
25
+ slot: typeof slot;
26
+ visible: import("vue").ComputedRef<FormAction[]>;
27
+ };
28
+ /** Footer group of an action at the current breakpoint. */
29
+ declare function slot(action: FormAction): "left" | "right";
30
+ /** True when an action fills its row at the current breakpoint. */
31
+ declare function fills(action: FormAction): boolean;
32
+ export {};
@@ -0,0 +1,89 @@
1
+ import { computed } from "vue";
2
+ import { getResponsiveValue } from "../../shared/composables/use-responsive-value.js";
3
+ import { createPublicFormApi } from "../utils/api.js";
4
+ import { invokeFormFunction, isString } from "../utils/predicate.js";
5
+ import { resolveFormText } from "../utils/text.js";
6
+ import { useFormUi } from "./use-form-ui.js";
7
+ export function useFormActionButtons(params) {
8
+ const formUi = useFormUi();
9
+ const context = computed(() => ({
10
+ actionPending: params.runtime.actionPending.value,
11
+ api: createPublicFormApi(params.runtime),
12
+ currentStep: params.runtime.currentStepIndex.value,
13
+ isFirstStep: params.runtime.isFirstStep.value,
14
+ isLastStep: params.runtime.isLastStep.value,
15
+ isMultiStep: params.runtime.isStepped.value
16
+ }));
17
+ const visible = computed(
18
+ () => params.actions().filter((action) => action.condition?.(context.value) ?? true)
19
+ );
20
+ async function run(action) {
21
+ if (action.key === "submit") {
22
+ return;
23
+ }
24
+ if (action.key === "next") {
25
+ await params.runtime.nextStep();
26
+ return;
27
+ }
28
+ if (action.key === "previous") {
29
+ await params.runtime.previousStep();
30
+ return;
31
+ }
32
+ if (action.key === "reset") {
33
+ await params.runtime.reset();
34
+ return;
35
+ }
36
+ if (action.key === "cancel") {
37
+ params.onCancel();
38
+ return;
39
+ }
40
+ if ("action" in action) {
41
+ await action.action?.(context.value);
42
+ }
43
+ }
44
+ function link(action) {
45
+ if (!("link" in action)) {
46
+ return;
47
+ }
48
+ if (isString(action.link)) {
49
+ return action.link;
50
+ }
51
+ const resolved = invokeFormFunction(action.link, [context.value]);
52
+ return isString(resolved) ? resolved : void 0;
53
+ }
54
+ function loading(action) {
55
+ const pending = params.runtime.actionPending.value;
56
+ return pending !== null && isBuiltInActionKey(action.key) && action.key === pending;
57
+ }
58
+ function disabled(action) {
59
+ if (action.disabled?.(context.value)) {
60
+ return true;
61
+ }
62
+ return params.runtime.actionPending.value !== null && !loading(action);
63
+ }
64
+ function button(action) {
65
+ const primary = action.type === "primary";
66
+ return {
67
+ color: action.color ?? (primary ? "primary" : void 0),
68
+ disabled: disabled(action),
69
+ icon: action.icon === false ? void 0 : action.icon,
70
+ label: resolveFormText(action.label),
71
+ loading: loading(action),
72
+ size: action.size ?? formUi.controlSize.value,
73
+ to: link(action),
74
+ trailingIcon: action.trailingIcon === false ? void 0 : action.trailingIcon,
75
+ type: action.key === "submit" ? "submit" : "button",
76
+ variant: action.variant ?? (primary ? "solid" : void 0)
77
+ };
78
+ }
79
+ return { button, fills, run, slot, visible };
80
+ }
81
+ function slot(action) {
82
+ return getResponsiveValue(action.slot ?? "right") === "left" ? "left" : "right";
83
+ }
84
+ function fills(action) {
85
+ return getResponsiveValue(action.width ?? "fit") === "fill";
86
+ }
87
+ function isBuiltInActionKey(value) {
88
+ return value === "reset" || value === "cancel" || value === "submit" || value === "previous" || value === "next";
89
+ }
@@ -1,6 +1,9 @@
1
- import type { ComputedRef } from 'vue';
2
- import type { FormAction, FormRenderShell, FormRuntime } from '../types/index.js';
3
- export declare function useFormActions(params: {
1
+ import type { FormAction, FormActionSlot, FormRuntime } from '../types/index.js';
2
+ interface UseFormActionsParams {
4
3
  runtime: FormRuntime;
5
- shell: ComputedRef<FormRenderShell>;
6
- }): ComputedRef<readonly FormAction[]>;
4
+ /** Slot the built-in actions take unless an authored action sets its own. */
5
+ slot: () => FormActionSlot;
6
+ }
7
+ /** Resolves the actions of the current schema or step, completing built-in actions. */
8
+ export declare function useFormActions(params: UseFormActionsParams): import("vue").ComputedRef<readonly FormAction[]>;
9
+ export {};
@@ -28,7 +28,7 @@ function normalizeFormAction(action, params, t) {
28
28
  };
29
29
  }
30
30
  function getBaseFormAction(key, params, t) {
31
- const slot = params.shell.value === "inline" ? "left" : "right";
31
+ const slot = params.slot();
32
32
  if (key === "submit") {
33
33
  return {
34
34
  condition: (context) => context.isMultiStep ? context.isLastStep : true,