@morscherlab/mld-sdk 0.6.5 → 0.7.1
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 +3 -0
- package/dist/composables/index.js +8 -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/composables/usePluginConfig.d.ts +12 -0
- package/dist/composables/usePluginConfig.js +77 -0
- package/dist/composables/usePluginConfig.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +247 -6
- 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 +7 -0
- package/src/composables/useFormBuilder.ts +382 -0
- package/src/composables/usePluginConfig.ts +92 -0
- package/src/index.ts +3 -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/components/number-input.css +4 -1
- 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,382 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
import { useForm, type FieldRules, type UseFormReturn } from './useForm'
|
|
3
|
+
import { getFieldRegistryEntry, getTypeDefault } from './formBuilderRegistry'
|
|
4
|
+
import type {
|
|
5
|
+
FormSchema,
|
|
6
|
+
FormFieldSchema,
|
|
7
|
+
FormSectionSchema,
|
|
8
|
+
FieldCondition,
|
|
9
|
+
FieldValidation,
|
|
10
|
+
FormEnhancements,
|
|
11
|
+
UseFormBuilderReturn,
|
|
12
|
+
} from '../types/form-builder'
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Condition evaluator
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Evaluate a JSON-serializable field condition against the current form data.
|
|
20
|
+
*
|
|
21
|
+
* Supports logical operators (`and`, `or`, `not`) and comparison operators
|
|
22
|
+
* (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
|
|
23
|
+
* `contains`). Returns `true` if the condition passes.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluateCondition(
|
|
26
|
+
condition: FieldCondition,
|
|
27
|
+
data: Record<string, unknown>,
|
|
28
|
+
): boolean {
|
|
29
|
+
if ('and' in condition) {
|
|
30
|
+
return condition.and.every((c) => evaluateCondition(c, data))
|
|
31
|
+
}
|
|
32
|
+
if ('or' in condition) {
|
|
33
|
+
return condition.or.some((c) => evaluateCondition(c, data))
|
|
34
|
+
}
|
|
35
|
+
if ('not' in condition) {
|
|
36
|
+
return !evaluateCondition(condition.not, data)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const value = data[condition.field]
|
|
40
|
+
|
|
41
|
+
if ('eq' in condition) return value === condition.eq
|
|
42
|
+
if ('neq' in condition) return value !== condition.neq
|
|
43
|
+
if ('gt' in condition) return typeof value === 'number' && value > condition.gt
|
|
44
|
+
if ('lt' in condition) return typeof value === 'number' && value < condition.lt
|
|
45
|
+
if ('gte' in condition) return typeof value === 'number' && value >= condition.gte
|
|
46
|
+
if ('lte' in condition) return typeof value === 'number' && value <= condition.lte
|
|
47
|
+
if ('in' in condition) return condition.in.includes(value)
|
|
48
|
+
if ('notIn' in condition) return !condition.notIn.includes(value)
|
|
49
|
+
if ('truthy' in condition) return !!value
|
|
50
|
+
if ('falsy' in condition) return !value
|
|
51
|
+
if ('contains' in condition) {
|
|
52
|
+
if (typeof value === 'string') return value.includes(condition.contains)
|
|
53
|
+
if (Array.isArray(value)) return value.includes(condition.contains)
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Schema helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/** Return all sections across steps (wizard) or directly from a flat schema. */
|
|
65
|
+
function collectSections(schema: FormSchema): FormSectionSchema[] {
|
|
66
|
+
return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Return all field schemas in schema order, flattening sections and steps. */
|
|
70
|
+
function flattenFields(schema: FormSchema): FormFieldSchema[] {
|
|
71
|
+
return collectSections(schema).flatMap((section) => section.fields)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
|
|
75
|
+
function convertValidation(v: FieldValidation): FieldRules {
|
|
76
|
+
const rules: FieldRules = {}
|
|
77
|
+
|
|
78
|
+
if (v.required !== undefined) rules.required = v.required
|
|
79
|
+
if (v.minLength !== undefined) rules.minLength = v.minLength
|
|
80
|
+
if (v.maxLength !== undefined) rules.maxLength = v.maxLength
|
|
81
|
+
if (v.min !== undefined) rules.min = v.min
|
|
82
|
+
if (v.max !== undefined) rules.max = v.max
|
|
83
|
+
if (v.email !== undefined) rules.email = v.email
|
|
84
|
+
|
|
85
|
+
if (v.pattern !== undefined) {
|
|
86
|
+
rules.pattern =
|
|
87
|
+
typeof v.pattern === 'string'
|
|
88
|
+
? new RegExp(v.pattern)
|
|
89
|
+
: { value: new RegExp(v.pattern.value), message: v.pattern.message }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return rules
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// useFormBuilder
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drive a `FormSchema` as reactive form state.
|
|
101
|
+
*
|
|
102
|
+
* Builds initial values from schema defaults and `initialData`, derives
|
|
103
|
+
* validation rules from `FieldValidation` descriptors and enhancement
|
|
104
|
+
* validators, evaluates `FieldCondition` expressions for field/section
|
|
105
|
+
* visibility, and wires wizard step navigation when `schema.steps` is set.
|
|
106
|
+
*
|
|
107
|
+
* @param schema - Declarative form or wizard schema.
|
|
108
|
+
* @param initialData - Values that override schema defaults.
|
|
109
|
+
* @param enhancements - TypeScript-only callbacks (dynamic options, validators,
|
|
110
|
+
* submit handler, transform, field-change watcher).
|
|
111
|
+
*/
|
|
112
|
+
export function useFormBuilder<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
113
|
+
schema: FormSchema,
|
|
114
|
+
initialData?: Partial<T>,
|
|
115
|
+
enhancements?: FormEnhancements<T>,
|
|
116
|
+
): UseFormBuilderReturn<T> {
|
|
117
|
+
const fields = flattenFields(schema)
|
|
118
|
+
const sections = collectSections(schema)
|
|
119
|
+
|
|
120
|
+
// -- Build initial values --------------------------------------------------
|
|
121
|
+
const initialValues = {} as Record<string, unknown>
|
|
122
|
+
for (const field of fields) {
|
|
123
|
+
const key = field.name
|
|
124
|
+
if (initialData && key in initialData) {
|
|
125
|
+
initialValues[key] = (initialData as Record<string, unknown>)[key]
|
|
126
|
+
} else if (field.defaultValue !== undefined) {
|
|
127
|
+
initialValues[key] = field.defaultValue
|
|
128
|
+
} else {
|
|
129
|
+
initialValues[key] = getTypeDefault(field.type)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -- Build validation rules ------------------------------------------------
|
|
134
|
+
const rules: Partial<Record<string, FieldRules>> = {}
|
|
135
|
+
for (const field of fields) {
|
|
136
|
+
const base: FieldRules = field.validation ? convertValidation(field.validation) : {}
|
|
137
|
+
const enhancement = enhancements?.fields?.[field.name as keyof T]
|
|
138
|
+
|
|
139
|
+
// Wrap validators to skip hidden fields
|
|
140
|
+
const customValidators: Array<(value: unknown, formData: Record<string, unknown>) => string | undefined | null> = []
|
|
141
|
+
|
|
142
|
+
if (enhancement?.validate) {
|
|
143
|
+
const fn = enhancement.validate
|
|
144
|
+
customValidators.push((value, formData) => fn(value, formData as T))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (customValidators.length > 0) {
|
|
148
|
+
base.custom = customValidators
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Wrap all rules to skip hidden fields
|
|
152
|
+
if (Object.keys(base).length > 0) {
|
|
153
|
+
rules[field.name] = base
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// -- Create form -----------------------------------------------------------
|
|
158
|
+
const form = useForm(initialValues as T, rules as Partial<Record<keyof T, FieldRules>>)
|
|
159
|
+
|
|
160
|
+
// -- Visibility ------------------------------------------------------------
|
|
161
|
+
function isFieldVisible(name: string): boolean {
|
|
162
|
+
const enhancement = enhancements?.fields?.[name as keyof T]
|
|
163
|
+
if (enhancement?.visible) {
|
|
164
|
+
return enhancement.visible(form.data as T)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const field = fields.find((f) => f.name === name)
|
|
168
|
+
if (field?.condition) {
|
|
169
|
+
return evaluateCondition(field.condition, form.data as Record<string, unknown>)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isSectionVisible(id: string): boolean {
|
|
176
|
+
const section = sections.find((s) => s.id === id)
|
|
177
|
+
if (section?.condition) {
|
|
178
|
+
return evaluateCondition(section.condition, form.data as Record<string, unknown>)
|
|
179
|
+
}
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// -- Resolved field props --------------------------------------------------
|
|
184
|
+
function getResolvedFieldProps(field: FormFieldSchema): Record<string, unknown> {
|
|
185
|
+
const entry = getFieldRegistryEntry(field.type)
|
|
186
|
+
const formProps = form.getFieldProps(field.name as keyof T)
|
|
187
|
+
|
|
188
|
+
const merged: Record<string, unknown> = {
|
|
189
|
+
...entry.defaults,
|
|
190
|
+
...(field.props ?? {}),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Dynamic enhancement props
|
|
194
|
+
const enhancement = enhancements?.fields?.[field.name as keyof T]
|
|
195
|
+
if (enhancement?.props) {
|
|
196
|
+
Object.assign(merged, enhancement.props(form.data as T))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Dynamic options
|
|
200
|
+
const options = getFieldOptions(field.name)
|
|
201
|
+
if (options) {
|
|
202
|
+
merged.options = options
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Form field bindings
|
|
206
|
+
if (entry.vModel) {
|
|
207
|
+
merged.modelValue = formProps.modelValue
|
|
208
|
+
merged['onUpdate:modelValue'] = formProps['onUpdate:modelValue']
|
|
209
|
+
}
|
|
210
|
+
merged.onBlur = formProps.onBlur
|
|
211
|
+
|
|
212
|
+
// Error as boolean for components that use boolean error prop
|
|
213
|
+
const errorMsg = formProps.error
|
|
214
|
+
if (errorMsg) {
|
|
215
|
+
merged.error = true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Schema-level props
|
|
219
|
+
if (field.placeholder) merged.placeholder = field.placeholder
|
|
220
|
+
if (field.size) merged.size = field.size
|
|
221
|
+
if (field.disabled) merged.disabled = true
|
|
222
|
+
if (field.readonly) merged.readonly = true
|
|
223
|
+
|
|
224
|
+
// Radio group needs a name prop
|
|
225
|
+
if (field.type === 'radio' && !merged.name) {
|
|
226
|
+
merged.name = field.name
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return merged
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getFieldOptions(name: string): { label: string; value: unknown }[] | undefined {
|
|
233
|
+
const enhancement = enhancements?.fields?.[name as keyof T]
|
|
234
|
+
if (enhancement?.options) {
|
|
235
|
+
return enhancement.options(form.data as T)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const field = fields.find((f) => f.name === name)
|
|
239
|
+
if (field?.props?.options) {
|
|
240
|
+
return field.props.options as { label: string; value: unknown }[]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return undefined
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// -- Wizard state ----------------------------------------------------------
|
|
247
|
+
const currentStep = ref(0)
|
|
248
|
+
|
|
249
|
+
const isCurrentStepValid = computed(() => {
|
|
250
|
+
if (!schema.steps) return form.isValid.value
|
|
251
|
+
|
|
252
|
+
const step = schema.steps[currentStep.value]
|
|
253
|
+
if (!step) return true
|
|
254
|
+
|
|
255
|
+
for (const section of step.sections) {
|
|
256
|
+
if (!isSectionVisible(section.id)) continue
|
|
257
|
+
for (const field of section.fields) {
|
|
258
|
+
if (!isFieldVisible(field.name)) continue
|
|
259
|
+
if (!form.validateField(field.name)) return false
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return true
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
function goNext(): boolean {
|
|
266
|
+
if (!schema.steps) return false
|
|
267
|
+
|
|
268
|
+
// Touch and validate all visible fields in current step
|
|
269
|
+
const step = schema.steps[currentStep.value]
|
|
270
|
+
if (!step) return false
|
|
271
|
+
|
|
272
|
+
let valid = true
|
|
273
|
+
for (const section of step.sections) {
|
|
274
|
+
if (!isSectionVisible(section.id)) continue
|
|
275
|
+
for (const field of section.fields) {
|
|
276
|
+
if (!isFieldVisible(field.name)) continue
|
|
277
|
+
form.setFieldTouched(field.name, true)
|
|
278
|
+
if (!form.validateField(field.name)) valid = false
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!valid) return false
|
|
283
|
+
|
|
284
|
+
if (currentStep.value < schema.steps.length - 1) {
|
|
285
|
+
currentStep.value++
|
|
286
|
+
}
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function goBack(): void {
|
|
291
|
+
if (currentStep.value > 0) {
|
|
292
|
+
currentStep.value--
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function goToStep(index: number): void {
|
|
297
|
+
if (!schema.steps) return
|
|
298
|
+
if (index >= 0 && index < schema.steps.length) {
|
|
299
|
+
currentStep.value = index
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// -- Validate (skip hidden fields) -----------------------------------------
|
|
304
|
+
function validate(): boolean {
|
|
305
|
+
let allValid = true
|
|
306
|
+
for (const field of fields) {
|
|
307
|
+
if (!isFieldVisible(field.name)) continue
|
|
308
|
+
form.setFieldTouched(field.name, true)
|
|
309
|
+
if (!form.validateField(field.name)) {
|
|
310
|
+
allValid = false
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return allValid
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// -- Submit ----------------------------------------------------------------
|
|
317
|
+
async function submit(): Promise<void> {
|
|
318
|
+
if (!validate()) return
|
|
319
|
+
|
|
320
|
+
// Build submission data excluding hidden fields
|
|
321
|
+
let submitData = {} as Record<string, unknown>
|
|
322
|
+
for (const field of fields) {
|
|
323
|
+
if (isFieldVisible(field.name)) {
|
|
324
|
+
submitData[field.name] = (form.data as Record<string, unknown>)[field.name]
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (enhancements?.transform) {
|
|
329
|
+
submitData = enhancements.transform(submitData as T) as Record<string, unknown>
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (enhancements?.onSubmit) {
|
|
333
|
+
form.isSubmitting.value = true
|
|
334
|
+
try {
|
|
335
|
+
await enhancements.onSubmit(submitData as T)
|
|
336
|
+
} finally {
|
|
337
|
+
form.isSubmitting.value = false
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// -- Reset -----------------------------------------------------------------
|
|
343
|
+
function reset(values?: Partial<T>): void {
|
|
344
|
+
form.reset(values)
|
|
345
|
+
currentStep.value = 0
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// -- onFieldChange wiring --------------------------------------------------
|
|
349
|
+
if (enhancements?.onFieldChange) {
|
|
350
|
+
const callback = enhancements.onFieldChange
|
|
351
|
+
watch(
|
|
352
|
+
() => ({ ...(form.data as Record<string, unknown>) }),
|
|
353
|
+
(newData, oldData) => {
|
|
354
|
+
if (!oldData) return
|
|
355
|
+
for (const key of Object.keys(newData)) {
|
|
356
|
+
if (newData[key] !== oldData[key]) {
|
|
357
|
+
callback(key as keyof T, newData[key], newData as T)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{ deep: true },
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
form: form as UseFormReturn<T>,
|
|
367
|
+
rules: rules as Partial<Record<keyof T, FieldRules>>,
|
|
368
|
+
isFieldVisible,
|
|
369
|
+
isSectionVisible,
|
|
370
|
+
fields,
|
|
371
|
+
getResolvedFieldProps,
|
|
372
|
+
getFieldOptions,
|
|
373
|
+
currentStep,
|
|
374
|
+
isCurrentStepValid,
|
|
375
|
+
goNext,
|
|
376
|
+
goBack,
|
|
377
|
+
goToStep,
|
|
378
|
+
validate,
|
|
379
|
+
reset,
|
|
380
|
+
submit,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import { useApi } from './useApi'
|
|
3
|
+
import { usePlatformContext } from './usePlatformContext'
|
|
4
|
+
|
|
5
|
+
export interface UsePluginConfigReturn {
|
|
6
|
+
config: Ref<Record<string, unknown>>
|
|
7
|
+
isLoading: Ref<boolean>
|
|
8
|
+
isSaving: Ref<boolean>
|
|
9
|
+
error: Ref<string | null>
|
|
10
|
+
isDirty: ComputedRef<boolean>
|
|
11
|
+
load: () => Promise<void>
|
|
12
|
+
save: () => Promise<boolean>
|
|
13
|
+
reset: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
17
|
+
const api = useApi()
|
|
18
|
+
const { plugin } = usePlatformContext()
|
|
19
|
+
|
|
20
|
+
const resolvedName = computed(() => pluginName ?? plugin.value?.name ?? '')
|
|
21
|
+
|
|
22
|
+
const config = ref<Record<string, unknown>>({})
|
|
23
|
+
const savedConfig = ref<Record<string, unknown>>({})
|
|
24
|
+
const isLoading = ref(false)
|
|
25
|
+
const isSaving = ref(false)
|
|
26
|
+
const error = ref<string | null>(null)
|
|
27
|
+
|
|
28
|
+
const isDirty = computed(() => {
|
|
29
|
+
return JSON.stringify(config.value) !== JSON.stringify(savedConfig.value)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
async function load(): Promise<void> {
|
|
33
|
+
const name = resolvedName.value
|
|
34
|
+
if (!name) return
|
|
35
|
+
|
|
36
|
+
isLoading.value = true
|
|
37
|
+
error.value = null
|
|
38
|
+
try {
|
|
39
|
+
const response = await api.get<{ plugin_name: string; config: Record<string, unknown> }>(
|
|
40
|
+
`/api/plugins/${encodeURIComponent(name)}/config`,
|
|
41
|
+
)
|
|
42
|
+
config.value = { ...response.config }
|
|
43
|
+
savedConfig.value = { ...response.config }
|
|
44
|
+
} catch (e) {
|
|
45
|
+
error.value = e instanceof Error ? e.message : 'Failed to load plugin config'
|
|
46
|
+
} finally {
|
|
47
|
+
isLoading.value = false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function save(): Promise<boolean> {
|
|
52
|
+
const name = resolvedName.value
|
|
53
|
+
if (!name) return false
|
|
54
|
+
|
|
55
|
+
isSaving.value = true
|
|
56
|
+
error.value = null
|
|
57
|
+
try {
|
|
58
|
+
const response = await api.put<{ plugin_name: string; config: Record<string, unknown> }>(
|
|
59
|
+
`/api/plugins/${encodeURIComponent(name)}/config`,
|
|
60
|
+
{ config: config.value },
|
|
61
|
+
)
|
|
62
|
+
config.value = { ...response.config }
|
|
63
|
+
savedConfig.value = { ...response.config }
|
|
64
|
+
return true
|
|
65
|
+
} catch (e) {
|
|
66
|
+
error.value = e instanceof Error ? e.message : 'Failed to save plugin config'
|
|
67
|
+
return false
|
|
68
|
+
} finally {
|
|
69
|
+
isSaving.value = false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function reset(): void {
|
|
74
|
+
config.value = { ...savedConfig.value }
|
|
75
|
+
error.value = null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
load()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
config,
|
|
84
|
+
isLoading,
|
|
85
|
+
isSaving,
|
|
86
|
+
error,
|
|
87
|
+
isDirty,
|
|
88
|
+
load,
|
|
89
|
+
save,
|
|
90
|
+
reset,
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
|
|
63
63
|
.mld-layout--floating .mld-layout__topbar {
|
|
64
64
|
border-radius: var(--radius-lg, 0.75rem);
|
|
65
|
-
overflow: hidden;
|
|
66
65
|
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
|
|
67
66
|
border: 1px solid var(--border-color, var(--mld-border, #e5e7eb));
|
|
68
67
|
}
|
|
@@ -94,6 +93,6 @@
|
|
|
94
93
|
border-bottom: none;
|
|
95
94
|
margin: 0;
|
|
96
95
|
width: 100%;
|
|
97
|
-
border-radius: 0;
|
|
96
|
+
border-radius: var(--radius-lg, 0.75rem);
|
|
98
97
|
box-shadow: none;
|
|
99
98
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* FormBuilder */
|
|
2
|
+
.mld-form-builder {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 1.5rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mld-form-builder--sm {
|
|
9
|
+
gap: 1rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.mld-form-builder--lg {
|
|
13
|
+
gap: 2rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.mld-form-builder__step {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 1.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* FormSection */
|
|
23
|
+
.mld-form-section {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
gap: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.mld-form-section--static {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: 1rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.mld-form-section__header {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 0.25rem;
|
|
39
|
+
margin-bottom: 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.mld-form-section__title {
|
|
43
|
+
font-size: var(--font-size-base, 0.9375rem);
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
color: var(--color-text, #1a1a2e);
|
|
46
|
+
margin: 0;
|
|
47
|
+
line-height: 1.4;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.mld-form-section__description {
|
|
51
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
52
|
+
color: var(--color-text-secondary, #6b7280);
|
|
53
|
+
margin: 0;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.mld-form-section__grid {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 1rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* FormActions */
|
|
63
|
+
.mld-form-actions {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 0.75rem;
|
|
67
|
+
padding-top: 1rem;
|
|
68
|
+
border-top: 1px solid var(--color-border, #e5e7eb);
|
|
69
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
.mld-number-input {
|
|
4
4
|
display: inline-flex;
|
|
5
|
+
max-width: 100%;
|
|
5
6
|
border-radius: 0.5rem;
|
|
6
7
|
border: 1px solid var(--border-color);
|
|
7
8
|
overflow: hidden;
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
display: flex;
|
|
43
44
|
align-items: center;
|
|
44
45
|
justify-content: center;
|
|
46
|
+
flex-shrink: 0;
|
|
45
47
|
background-color: var(--bg-tertiary);
|
|
46
48
|
color: var(--text-muted);
|
|
47
49
|
border: none;
|
|
@@ -85,7 +87,8 @@
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
.mld-number-input__input {
|
|
88
|
-
|
|
90
|
+
flex: 1;
|
|
91
|
+
min-width: 0;
|
|
89
92
|
text-align: center;
|
|
90
93
|
background-color: var(--bg-secondary);
|
|
91
94
|
color: var(--text-primary);
|
package/src/styles/index.css
CHANGED