@morscherlab/mld-sdk 0.6.4 → 0.7.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/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
- package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
- package/dist/components/BaseButton.vue.d.ts +1 -1
- package/dist/components/BasePill.vue.d.ts +1 -1
- package/dist/components/DropdownButton.vue.d.ts +1 -1
- package/dist/components/FormActions.vue.d.ts +33 -0
- package/dist/components/FormActions.vue.js +76 -0
- package/dist/components/FormActions.vue.js.map +1 -0
- package/dist/components/FormActions.vue3.js +6 -0
- package/dist/components/FormActions.vue3.js.map +1 -0
- package/dist/components/FormBuilder.vue.js +205 -0
- package/dist/components/FormBuilder.vue.js.map +1 -0
- package/dist/components/FormBuilder.vue3.js +6 -0
- package/dist/components/FormBuilder.vue3.js.map +1 -0
- package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
- package/dist/components/FormFieldRenderer.vue.js +48 -0
- package/dist/components/FormFieldRenderer.vue.js.map +1 -0
- package/dist/components/FormFieldRenderer.vue2.js +5 -0
- package/dist/components/FormFieldRenderer.vue2.js.map +1 -0
- package/dist/components/FormSection.vue.d.ts +43 -0
- package/dist/components/FormSection.vue.js +117 -0
- package/dist/components/FormSection.vue.js.map +1 -0
- package/dist/components/FormSection.vue3.js +6 -0
- package/dist/components/FormSection.vue3.js.map +1 -0
- package/dist/components/IconButton.vue.d.ts +1 -1
- package/dist/components/LoadingSpinner.vue.d.ts +1 -1
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ReagentList.vue.d.ts +2 -2
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SegmentedControl.vue.d.ts +1 -1
- package/dist/components/WellEditPopup.vue.d.ts +2 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +19 -8
- package/dist/components/index.js.map +1 -1
- package/dist/composables/formBuilderRegistry.d.ts +13 -0
- package/dist/composables/formBuilderRegistry.js +87 -0
- package/dist/composables/formBuilderRegistry.js.map +1 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +6 -0
- package/dist/composables/index.js.map +1 -1
- package/dist/composables/useFormBuilder.d.ts +23 -0
- package/dist/composables/useFormBuilder.js +264 -0
- package/dist/composables/useFormBuilder.js.map +1 -0
- package/dist/styles.css +239 -4
- package/dist/types/form-builder.d.ts +167 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
- package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
- package/src/components/FormActions.vue +92 -0
- package/src/components/FormBuilder.vue +214 -0
- package/src/components/FormFieldRenderer.vue +58 -0
- package/src/components/FormSection.vue +90 -0
- package/src/components/index.ts +6 -0
- package/src/composables/formBuilderRegistry.ts +79 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useFormBuilder.ts +382 -0
- package/src/styles/components/app-container.css +1 -0
- package/src/styles/components/app-layout.css +1 -2
- package/src/styles/components/form-builder.css +69 -0
- package/src/styles/index.css +1 -0
- package/src/types/form-builder.ts +197 -0
- package/src/types/index.ts +14 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Action bar rendered at the bottom of a FormBuilder.
|
|
4
|
+
*
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* - Flat form: shows an optional Cancel button and a Submit button.
|
|
7
|
+
* - Wizard (`isWizard=true`): shows Back / Next on intermediate steps and
|
|
8
|
+
* Submit only on the last step. Back is hidden on the first step.
|
|
9
|
+
*
|
|
10
|
+
* The `canProceed` prop gates the primary action (Next or Submit) to prevent
|
|
11
|
+
* advancing when the current step has validation errors.
|
|
12
|
+
*/
|
|
13
|
+
import BaseButton from './BaseButton.vue'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
isWizard?: boolean
|
|
17
|
+
isFirst?: boolean
|
|
18
|
+
isLast?: boolean
|
|
19
|
+
canProceed?: boolean
|
|
20
|
+
loading?: boolean
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
submitLabel?: string
|
|
23
|
+
cancelLabel?: string
|
|
24
|
+
showCancel?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
withDefaults(defineProps<Props>(), {
|
|
28
|
+
isWizard: false,
|
|
29
|
+
isFirst: true,
|
|
30
|
+
isLast: true,
|
|
31
|
+
canProceed: true,
|
|
32
|
+
loading: false,
|
|
33
|
+
disabled: false,
|
|
34
|
+
submitLabel: 'Submit',
|
|
35
|
+
cancelLabel: 'Cancel',
|
|
36
|
+
showCancel: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
submit: []
|
|
41
|
+
cancel: []
|
|
42
|
+
back: []
|
|
43
|
+
next: []
|
|
44
|
+
}>()
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<div class="mld-form-actions">
|
|
49
|
+
<BaseButton
|
|
50
|
+
v-if="showCancel"
|
|
51
|
+
variant="ghost"
|
|
52
|
+
:disabled="loading || disabled"
|
|
53
|
+
@click="emit('cancel')"
|
|
54
|
+
>
|
|
55
|
+
{{ cancelLabel }}
|
|
56
|
+
</BaseButton>
|
|
57
|
+
|
|
58
|
+
<div style="flex: 1" />
|
|
59
|
+
|
|
60
|
+
<BaseButton
|
|
61
|
+
v-if="isWizard && !isFirst"
|
|
62
|
+
variant="secondary"
|
|
63
|
+
:disabled="loading || disabled"
|
|
64
|
+
@click="emit('back')"
|
|
65
|
+
>
|
|
66
|
+
Back
|
|
67
|
+
</BaseButton>
|
|
68
|
+
|
|
69
|
+
<BaseButton
|
|
70
|
+
v-if="isWizard && !isLast"
|
|
71
|
+
variant="primary"
|
|
72
|
+
:disabled="!canProceed || loading || disabled"
|
|
73
|
+
@click="emit('next')"
|
|
74
|
+
>
|
|
75
|
+
Next
|
|
76
|
+
</BaseButton>
|
|
77
|
+
|
|
78
|
+
<BaseButton
|
|
79
|
+
v-if="!isWizard || isLast"
|
|
80
|
+
variant="primary"
|
|
81
|
+
:loading="loading"
|
|
82
|
+
:disabled="!canProceed || loading || disabled"
|
|
83
|
+
@click="emit('submit')"
|
|
84
|
+
>
|
|
85
|
+
{{ submitLabel }}
|
|
86
|
+
</BaseButton>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
@import '../styles/components/form-builder.css';
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Schema-driven form component that renders flat or multi-step wizard forms.
|
|
4
|
+
*
|
|
5
|
+
* Pass a `FormSchema` with either `sections` (flat) or `steps` (wizard).
|
|
6
|
+
* Use `v-model` for two-way binding of the form data; the `submit` event
|
|
7
|
+
* carries only the data for currently-visible fields. Supply `enhancements`
|
|
8
|
+
* for dynamic options, custom validators, a submit handler, and field-change
|
|
9
|
+
* callbacks that cannot be expressed in JSON.
|
|
10
|
+
*
|
|
11
|
+
* Exposes `form`, `validate`, `reset`, `goNext`, `goBack`, `goToStep`, and
|
|
12
|
+
* `builder` for imperative control via template refs.
|
|
13
|
+
*
|
|
14
|
+
* Slots:
|
|
15
|
+
* - `field:<name>` — override a single field's input component
|
|
16
|
+
* - `section:<id>` — replace an entire section body
|
|
17
|
+
* - `section:<id>:after` — inject content after a section
|
|
18
|
+
* - `actions` — replace the default FormActions bar
|
|
19
|
+
*/
|
|
20
|
+
import { ref, computed, watch } from 'vue'
|
|
21
|
+
import type { FormSchema, FormEnhancements, UseFormBuilderReturn } from '../types/form-builder'
|
|
22
|
+
import type { WizardStep } from '../types'
|
|
23
|
+
import { useFormBuilder } from '../composables/useFormBuilder'
|
|
24
|
+
import StepWizard from './StepWizard.vue'
|
|
25
|
+
import FormSection from './FormSection.vue'
|
|
26
|
+
import FormActions from './FormActions.vue'
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
schema: FormSchema
|
|
30
|
+
modelValue?: Record<string, unknown>
|
|
31
|
+
enhancements?: FormEnhancements<Record<string, unknown>>
|
|
32
|
+
loading?: boolean
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
size?: 'sm' | 'md' | 'lg'
|
|
35
|
+
readonly?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
39
|
+
loading: false,
|
|
40
|
+
disabled: false,
|
|
41
|
+
readonly: false,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits<{
|
|
45
|
+
'update:modelValue': [data: Record<string, unknown>]
|
|
46
|
+
submit: [data: Record<string, unknown>]
|
|
47
|
+
cancel: []
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const builder = useFormBuilder(
|
|
51
|
+
props.schema,
|
|
52
|
+
props.modelValue,
|
|
53
|
+
props.enhancements,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Sync modelValue changes back to parent
|
|
57
|
+
watch(
|
|
58
|
+
() => ({ ...builder.form.data }),
|
|
59
|
+
(data) => emit('update:modelValue', data as Record<string, unknown>),
|
|
60
|
+
{ deep: true },
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Wizard support
|
|
64
|
+
const isWizard = computed(() => !!props.schema.steps)
|
|
65
|
+
const wizardRef = ref<InstanceType<typeof StepWizard> | null>(null)
|
|
66
|
+
|
|
67
|
+
const wizardSteps = computed<WizardStep[]>(() => {
|
|
68
|
+
if (!props.schema.steps) return []
|
|
69
|
+
return props.schema.steps.map((step) => ({
|
|
70
|
+
id: step.id,
|
|
71
|
+
label: step.label,
|
|
72
|
+
description: step.description,
|
|
73
|
+
icon: step.icon,
|
|
74
|
+
optional: step.optional,
|
|
75
|
+
}))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Sync step validation state with StepWizard
|
|
79
|
+
watch(
|
|
80
|
+
() => builder.isCurrentStepValid.value,
|
|
81
|
+
(valid) => {
|
|
82
|
+
wizardRef.value?.setStepValid(builder.currentStep.value, valid)
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
function handleNext() {
|
|
87
|
+
const success = builder.goNext()
|
|
88
|
+
if (success) {
|
|
89
|
+
wizardRef.value?.setStepValid(builder.currentStep.value - 1, true)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleSubmit() {
|
|
94
|
+
await builder.submit()
|
|
95
|
+
const formData = builder.form.data as Record<string, unknown>
|
|
96
|
+
const visibleData = Object.fromEntries(
|
|
97
|
+
builder.fields
|
|
98
|
+
.filter((f) => builder.isFieldVisible(f.name))
|
|
99
|
+
.map((f) => [f.name, formData[f.name]]),
|
|
100
|
+
)
|
|
101
|
+
emit('submit', visibleData)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleCancel() {
|
|
105
|
+
emit('cancel')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
defineExpose({
|
|
109
|
+
form: builder.form,
|
|
110
|
+
validate: builder.validate,
|
|
111
|
+
reset: builder.reset,
|
|
112
|
+
goNext: builder.goNext,
|
|
113
|
+
goBack: builder.goBack,
|
|
114
|
+
goToStep: builder.goToStep,
|
|
115
|
+
builder,
|
|
116
|
+
})
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<div :class="['mld-form-builder', size ? `mld-form-builder--${size}` : '']">
|
|
121
|
+
<!-- Wizard mode -->
|
|
122
|
+
<template v-if="isWizard && schema.steps">
|
|
123
|
+
<StepWizard
|
|
124
|
+
ref="wizardRef"
|
|
125
|
+
:steps="wizardSteps"
|
|
126
|
+
:model-value="builder.currentStep.value"
|
|
127
|
+
@update:model-value="builder.goToStep($event)"
|
|
128
|
+
>
|
|
129
|
+
<template v-for="step in schema.steps" :key="step.id" #[`step-${step.id}`]>
|
|
130
|
+
<div class="mld-form-builder__step">
|
|
131
|
+
<template v-for="section in step.sections" :key="section.id">
|
|
132
|
+
<FormSection
|
|
133
|
+
v-if="builder.isSectionVisible(section.id)"
|
|
134
|
+
:section="section"
|
|
135
|
+
:builder="(builder as UseFormBuilderReturn<Record<string, unknown>>)"
|
|
136
|
+
>
|
|
137
|
+
<!-- Forward field slots -->
|
|
138
|
+
<template v-for="field in section.fields" :key="field.name" #[`field:${field.name}`]="slotProps">
|
|
139
|
+
<slot :name="`field:${field.name}`" v-bind="slotProps" />
|
|
140
|
+
</template>
|
|
141
|
+
<!-- Forward section slots -->
|
|
142
|
+
<template #[`section:${section.id}`]="slotProps">
|
|
143
|
+
<slot :name="`section:${section.id}`" v-bind="slotProps" />
|
|
144
|
+
</template>
|
|
145
|
+
<template #[`section:${section.id}:after`]="slotProps">
|
|
146
|
+
<slot :name="`section:${section.id}:after`" v-bind="slotProps" />
|
|
147
|
+
</template>
|
|
148
|
+
</FormSection>
|
|
149
|
+
</template>
|
|
150
|
+
</div>
|
|
151
|
+
</template>
|
|
152
|
+
|
|
153
|
+
<template #navigation="{ isFirst, isLast }">
|
|
154
|
+
<slot name="actions" :form="builder.form" :builder="builder">
|
|
155
|
+
<FormActions
|
|
156
|
+
is-wizard
|
|
157
|
+
:is-first="isFirst"
|
|
158
|
+
:is-last="isLast"
|
|
159
|
+
:can-proceed="builder.isCurrentStepValid.value"
|
|
160
|
+
:loading="loading || builder.form.isSubmitting.value"
|
|
161
|
+
:disabled="disabled"
|
|
162
|
+
:submit-label="schema.submitLabel ?? 'Submit'"
|
|
163
|
+
:cancel-label="schema.cancelLabel ?? 'Cancel'"
|
|
164
|
+
:show-cancel="schema.showCancel ?? false"
|
|
165
|
+
@next="handleNext"
|
|
166
|
+
@back="builder.goBack"
|
|
167
|
+
@submit="handleSubmit"
|
|
168
|
+
@cancel="handleCancel"
|
|
169
|
+
/>
|
|
170
|
+
</slot>
|
|
171
|
+
</template>
|
|
172
|
+
</StepWizard>
|
|
173
|
+
</template>
|
|
174
|
+
|
|
175
|
+
<!-- Flat mode -->
|
|
176
|
+
<template v-else-if="schema.sections">
|
|
177
|
+
<template v-for="section in schema.sections" :key="section.id">
|
|
178
|
+
<FormSection
|
|
179
|
+
v-if="builder.isSectionVisible(section.id)"
|
|
180
|
+
:section="section"
|
|
181
|
+
:builder="(builder as UseFormBuilderReturn<Record<string, unknown>>)"
|
|
182
|
+
>
|
|
183
|
+
<!-- Forward field slots -->
|
|
184
|
+
<template v-for="field in section.fields" :key="field.name" #[`field:${field.name}`]="slotProps">
|
|
185
|
+
<slot :name="`field:${field.name}`" v-bind="slotProps" />
|
|
186
|
+
</template>
|
|
187
|
+
<!-- Forward section slots -->
|
|
188
|
+
<template #[`section:${section.id}`]="slotProps">
|
|
189
|
+
<slot :name="`section:${section.id}`" v-bind="slotProps" />
|
|
190
|
+
</template>
|
|
191
|
+
<template #[`section:${section.id}:after`]="slotProps">
|
|
192
|
+
<slot :name="`section:${section.id}:after`" v-bind="slotProps" />
|
|
193
|
+
</template>
|
|
194
|
+
</FormSection>
|
|
195
|
+
</template>
|
|
196
|
+
|
|
197
|
+
<slot name="actions" :form="builder.form" :builder="builder">
|
|
198
|
+
<FormActions
|
|
199
|
+
:loading="loading || builder.form.isSubmitting.value"
|
|
200
|
+
:disabled="disabled"
|
|
201
|
+
:submit-label="schema.submitLabel ?? 'Submit'"
|
|
202
|
+
:cancel-label="schema.cancelLabel ?? 'Cancel'"
|
|
203
|
+
:show-cancel="schema.showCancel ?? false"
|
|
204
|
+
@submit="handleSubmit"
|
|
205
|
+
@cancel="handleCancel"
|
|
206
|
+
/>
|
|
207
|
+
</slot>
|
|
208
|
+
</template>
|
|
209
|
+
</div>
|
|
210
|
+
</template>
|
|
211
|
+
|
|
212
|
+
<style>
|
|
213
|
+
@import '../styles/components/form-builder.css';
|
|
214
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Renders a single form field inside a FormField wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the SDK component for the field type from the registry, shows
|
|
6
|
+
* validation errors only after the field has been touched, and handles the
|
|
7
|
+
* FileUploader's event-based upload pattern via `handleUpload`. Consumers can
|
|
8
|
+
* override rendering via the `field:<name>` slot.
|
|
9
|
+
*/
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
import type { FormFieldSchema } from '../types/form-builder'
|
|
12
|
+
import type { UseFormReturn } from '../composables/useForm'
|
|
13
|
+
import { getFieldRegistryEntry } from '../composables/formBuilderRegistry'
|
|
14
|
+
import FormField from './FormField.vue'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
field: FormFieldSchema
|
|
18
|
+
resolvedProps: Record<string, unknown>
|
|
19
|
+
form: UseFormReturn<Record<string, unknown>>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = defineProps<Props>()
|
|
23
|
+
|
|
24
|
+
const entry = computed(() => getFieldRegistryEntry(props.field.type))
|
|
25
|
+
|
|
26
|
+
const errorMessage = computed(() => {
|
|
27
|
+
const name = props.field.name
|
|
28
|
+
return props.form.touched[name] ? props.form.errors[name] : null
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function handleUpload(files: File[]) {
|
|
32
|
+
props.form.setFieldValue(props.field.name, files)
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<FormField
|
|
38
|
+
:label="field.label"
|
|
39
|
+
:error="errorMessage ?? undefined"
|
|
40
|
+
:hint="field.hint"
|
|
41
|
+
:required="!!field.validation?.required"
|
|
42
|
+
:html-for="field.name"
|
|
43
|
+
>
|
|
44
|
+
<slot :name="`field:${field.name}`" :field="field" :form="form" :field-props="form.getFieldProps(field.name)">
|
|
45
|
+
<component
|
|
46
|
+
:is="entry.component"
|
|
47
|
+
v-if="entry.vModel"
|
|
48
|
+
v-bind="resolvedProps"
|
|
49
|
+
/>
|
|
50
|
+
<component
|
|
51
|
+
:is="entry.component"
|
|
52
|
+
v-else
|
|
53
|
+
v-bind="resolvedProps"
|
|
54
|
+
@upload="handleUpload"
|
|
55
|
+
/>
|
|
56
|
+
</slot>
|
|
57
|
+
</FormField>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Renders a single `FormSectionSchema` inside a FormBuilder.
|
|
4
|
+
*
|
|
5
|
+
* Filters out hidden fields before rendering, applies the section's column
|
|
6
|
+
* grid and per-field `colSpan`, and switches between a collapsible
|
|
7
|
+
* (`CollapsibleCard`) and a static layout based on `section.collapsible`.
|
|
8
|
+
* The entire section is hidden when all its fields are hidden. Consumers can
|
|
9
|
+
* replace the section body via `section:<id>` or inject content after it via
|
|
10
|
+
* `section:<id>:after`, and override individual fields via `field:<name>`.
|
|
11
|
+
*/
|
|
12
|
+
import { computed } from 'vue'
|
|
13
|
+
import type { FormSectionSchema, UseFormBuilderReturn } from '../types/form-builder'
|
|
14
|
+
import CollapsibleCard from './CollapsibleCard.vue'
|
|
15
|
+
import FormFieldRenderer from './FormFieldRenderer.vue'
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
section: FormSectionSchema
|
|
19
|
+
builder: UseFormBuilderReturn<Record<string, unknown>>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = defineProps<Props>()
|
|
23
|
+
|
|
24
|
+
const visibleFields = computed(() =>
|
|
25
|
+
props.section.fields.filter((f) => props.builder.isFieldVisible(f.name)),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const gridStyle = computed(() => ({
|
|
29
|
+
gridTemplateColumns: `repeat(${props.section.columns ?? 1}, 1fr)`,
|
|
30
|
+
}))
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div v-if="visibleFields.length > 0" class="mld-form-section">
|
|
35
|
+
<slot :name="`section:${section.id}`" :section="section" :form="builder.form">
|
|
36
|
+
<!-- Collapsible variant -->
|
|
37
|
+
<CollapsibleCard
|
|
38
|
+
v-if="section.collapsible"
|
|
39
|
+
:title="section.title"
|
|
40
|
+
:subtitle="section.description"
|
|
41
|
+
:default-open="section.defaultOpen ?? true"
|
|
42
|
+
>
|
|
43
|
+
<div class="mld-form-section__grid" :style="gridStyle">
|
|
44
|
+
<div
|
|
45
|
+
v-for="field in visibleFields"
|
|
46
|
+
:key="field.name"
|
|
47
|
+
:style="field.colSpan ? { gridColumn: `span ${field.colSpan}` } : undefined"
|
|
48
|
+
>
|
|
49
|
+
<slot :name="`field:${field.name}`" :field="field" :form="builder.form" :field-props="builder.form.getFieldProps(field.name)">
|
|
50
|
+
<FormFieldRenderer
|
|
51
|
+
:field="field"
|
|
52
|
+
:resolved-props="builder.getResolvedFieldProps(field)"
|
|
53
|
+
:form="builder.form"
|
|
54
|
+
/>
|
|
55
|
+
</slot>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</CollapsibleCard>
|
|
59
|
+
|
|
60
|
+
<!-- Static variant -->
|
|
61
|
+
<div v-else class="mld-form-section--static">
|
|
62
|
+
<div v-if="section.title || section.description" class="mld-form-section__header">
|
|
63
|
+
<h3 v-if="section.title" class="mld-form-section__title">{{ section.title }}</h3>
|
|
64
|
+
<p v-if="section.description" class="mld-form-section__description">{{ section.description }}</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="mld-form-section__grid" :style="gridStyle">
|
|
67
|
+
<div
|
|
68
|
+
v-for="field in visibleFields"
|
|
69
|
+
:key="field.name"
|
|
70
|
+
:style="field.colSpan ? { gridColumn: `span ${field.colSpan}` } : undefined"
|
|
71
|
+
>
|
|
72
|
+
<slot :name="`field:${field.name}`" :field="field" :form="builder.form" :field-props="builder.form.getFieldProps(field.name)">
|
|
73
|
+
<FormFieldRenderer
|
|
74
|
+
:field="field"
|
|
75
|
+
:resolved-props="builder.getResolvedFieldProps(field)"
|
|
76
|
+
:form="builder.form"
|
|
77
|
+
/>
|
|
78
|
+
</slot>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</slot>
|
|
83
|
+
|
|
84
|
+
<slot :name="`section:${section.id}:after`" :form="builder.form" />
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
@import '../styles/components/form-builder.css';
|
|
90
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -89,6 +89,12 @@ export { default as StepWizard } from './StepWizard.vue'
|
|
|
89
89
|
export { default as AuditTrail } from './AuditTrail.vue'
|
|
90
90
|
export { default as BatchProgressList } from './BatchProgressList.vue'
|
|
91
91
|
|
|
92
|
+
// Form builder components
|
|
93
|
+
export { default as FormBuilder } from './FormBuilder.vue'
|
|
94
|
+
export { default as FormSection } from './FormSection.vue'
|
|
95
|
+
export { default as FormActions } from './FormActions.vue'
|
|
96
|
+
export { default as FormFieldRenderer } from './FormFieldRenderer.vue'
|
|
97
|
+
|
|
92
98
|
// Scheduling / booking components
|
|
93
99
|
export { default as DateTimePicker } from './DateTimePicker.vue'
|
|
94
100
|
export { default as TimeRangeInput } from './TimeRangeInput.vue'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import type { FormFieldType } from '../types/form-builder'
|
|
3
|
+
|
|
4
|
+
import BaseInput from '../components/BaseInput.vue'
|
|
5
|
+
import BaseTextarea from '../components/BaseTextarea.vue'
|
|
6
|
+
import BaseSelect from '../components/BaseSelect.vue'
|
|
7
|
+
import MultiSelect from '../components/MultiSelect.vue'
|
|
8
|
+
import BaseCheckbox from '../components/BaseCheckbox.vue'
|
|
9
|
+
import BaseToggle from '../components/BaseToggle.vue'
|
|
10
|
+
import BaseRadioGroup from '../components/BaseRadioGroup.vue'
|
|
11
|
+
import BaseSlider from '../components/BaseSlider.vue'
|
|
12
|
+
import TagsInput from '../components/TagsInput.vue'
|
|
13
|
+
import NumberInput from '../components/NumberInput.vue'
|
|
14
|
+
import DatePicker from '../components/DatePicker.vue'
|
|
15
|
+
import TimePicker from '../components/TimePicker.vue'
|
|
16
|
+
import DateTimePicker from '../components/DateTimePicker.vue'
|
|
17
|
+
import FileUploader from '../components/FileUploader.vue'
|
|
18
|
+
import FormulaInput from '../components/FormulaInput.vue'
|
|
19
|
+
import SequenceInput from '../components/SequenceInput.vue'
|
|
20
|
+
import MoleculeInput from '../components/MoleculeInput.vue'
|
|
21
|
+
import ConcentrationInput from '../components/ConcentrationInput.vue'
|
|
22
|
+
import UnitInput from '../components/UnitInput.vue'
|
|
23
|
+
|
|
24
|
+
export interface RegistryEntry {
|
|
25
|
+
component: Component
|
|
26
|
+
/** Default props applied to the component. */
|
|
27
|
+
defaults: Record<string, unknown>
|
|
28
|
+
/** Whether the component uses v-model (false for event-only components like FileUploader). */
|
|
29
|
+
vModel: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const registry: Record<FormFieldType, RegistryEntry> = {
|
|
33
|
+
text: { component: BaseInput, defaults: { type: 'text' }, vModel: true },
|
|
34
|
+
email: { component: BaseInput, defaults: { type: 'email' }, vModel: true },
|
|
35
|
+
password: { component: BaseInput, defaults: { type: 'password' }, vModel: true },
|
|
36
|
+
tel: { component: BaseInput, defaults: { type: 'tel' }, vModel: true },
|
|
37
|
+
url: { component: BaseInput, defaults: { type: 'url' }, vModel: true },
|
|
38
|
+
search: { component: BaseInput, defaults: { type: 'search' }, vModel: true },
|
|
39
|
+
number: { component: NumberInput, defaults: {}, vModel: true },
|
|
40
|
+
textarea: { component: BaseTextarea, defaults: {}, vModel: true },
|
|
41
|
+
select: { component: BaseSelect, defaults: {}, vModel: true },
|
|
42
|
+
multiselect: { component: MultiSelect, defaults: {}, vModel: true },
|
|
43
|
+
checkbox: { component: BaseCheckbox, defaults: {}, vModel: true },
|
|
44
|
+
toggle: { component: BaseToggle, defaults: {}, vModel: true },
|
|
45
|
+
radio: { component: BaseRadioGroup, defaults: {}, vModel: true },
|
|
46
|
+
slider: { component: BaseSlider, defaults: {}, vModel: true },
|
|
47
|
+
tags: { component: TagsInput, defaults: {}, vModel: true },
|
|
48
|
+
date: { component: DatePicker, defaults: {}, vModel: true },
|
|
49
|
+
time: { component: TimePicker, defaults: {}, vModel: true },
|
|
50
|
+
datetime: { component: DateTimePicker, defaults: {}, vModel: true },
|
|
51
|
+
file: { component: FileUploader, defaults: {}, vModel: false },
|
|
52
|
+
formula: { component: FormulaInput, defaults: {}, vModel: true },
|
|
53
|
+
sequence: { component: SequenceInput, defaults: {}, vModel: true },
|
|
54
|
+
molecule: { component: MoleculeInput, defaults: {}, vModel: true },
|
|
55
|
+
concentration: { component: ConcentrationInput, defaults: {}, vModel: true },
|
|
56
|
+
unit: { component: UnitInput, defaults: {}, vModel: true },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Return the registry entry for a given field type. Throws if the type is unregistered. */
|
|
60
|
+
export function getFieldRegistryEntry(type: FormFieldType): RegistryEntry {
|
|
61
|
+
return registry[type]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Get the default empty value for a given field type. */
|
|
65
|
+
export function getTypeDefault(type: FormFieldType): unknown {
|
|
66
|
+
switch (type) {
|
|
67
|
+
case 'checkbox':
|
|
68
|
+
case 'toggle':
|
|
69
|
+
return false
|
|
70
|
+
case 'number':
|
|
71
|
+
case 'slider':
|
|
72
|
+
return undefined
|
|
73
|
+
case 'multiselect':
|
|
74
|
+
case 'tags':
|
|
75
|
+
return []
|
|
76
|
+
default:
|
|
77
|
+
return ''
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -85,3 +85,9 @@ export {
|
|
|
85
85
|
compareTime,
|
|
86
86
|
} from './useTimeUtils'
|
|
87
87
|
export { useScheduleDrag } from './useScheduleDrag'
|
|
88
|
+
export { useFormBuilder, evaluateCondition } from './useFormBuilder'
|
|
89
|
+
export {
|
|
90
|
+
getFieldRegistryEntry,
|
|
91
|
+
getTypeDefault,
|
|
92
|
+
type RegistryEntry,
|
|
93
|
+
} from './formBuilderRegistry'
|