refactor(form-schema): extract schema types and schema-driven behaviors to shared package
Moves formBuilder types, formValidation, useConditionalLogic, useFormSteps, and formatFieldValue from apps/portal/src to packages/form-schema/src. Adds @form-schema path alias to both apps/portal and apps/app. Vue field components remain per-app to allow independent visual evolution. Behavior-neutral: all 35 Vitest tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,149 +0,0 @@
|
||||
import { emailValidator, regexValidator, requiredValidator, urlValidator } from '@core/utils/validators'
|
||||
import { FormFieldType } from '@/types/formBuilder'
|
||||
import type { PublicFormField } from '@/types/formBuilder'
|
||||
|
||||
export type Validator = (value: unknown) => true | string
|
||||
|
||||
/**
|
||||
* Build a list of client-side validators for a public form field.
|
||||
* Runs in VTextField `:rules` and the stepper "current step valid"
|
||||
* gate. Mirrors (a subset of) the backend relaxed rule set so the
|
||||
* submitter gets feedback before the submit round-trip.
|
||||
*/
|
||||
export function getValidatorsForField(field: PublicFormField): Validator[] {
|
||||
const rules: Validator[] = []
|
||||
const v = field.validation_rules ?? {}
|
||||
|
||||
if (field.is_required) {
|
||||
rules.push(value => {
|
||||
const ok = requiredValidator(value)
|
||||
|
||||
return ok === true ? true : 'Dit veld is verplicht.'
|
||||
})
|
||||
}
|
||||
|
||||
switch (field.field_type) {
|
||||
case FormFieldType.EMAIL:
|
||||
rules.push(value => {
|
||||
const ok = emailValidator(value)
|
||||
|
||||
return ok === true ? true : 'Vul een geldig e-mailadres in.'
|
||||
})
|
||||
break
|
||||
|
||||
case FormFieldType.URL:
|
||||
rules.push(value => {
|
||||
const ok = urlValidator(value)
|
||||
|
||||
return ok === true ? true : 'Vul een geldige URL in (beginnend met http:// of https://).'
|
||||
})
|
||||
break
|
||||
|
||||
case FormFieldType.PHONE:
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
const s = String(value).replace(/\s+/g, '')
|
||||
|
||||
return /^\+?[\d()-]{6,}$/.test(s) || 'Vul een geldig telefoonnummer in.'
|
||||
})
|
||||
break
|
||||
|
||||
case FormFieldType.NUMBER:
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
const n = Number(value)
|
||||
|
||||
return Number.isFinite(n) || 'Vul een geldig getal in.'
|
||||
})
|
||||
if (typeof v.min === 'number') {
|
||||
const min = v.min
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
const n = Number(value)
|
||||
|
||||
return Number.isFinite(n) && n >= min ? true : `Minimaal ${min}.`
|
||||
})
|
||||
}
|
||||
if (typeof v.max === 'number') {
|
||||
const max = v.max
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
const n = Number(value)
|
||||
|
||||
return Number.isFinite(n) && n <= max ? true : `Maximaal ${max}.`
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case FormFieldType.TEXT:
|
||||
case FormFieldType.TEXTAREA:
|
||||
if (typeof v.min === 'number') {
|
||||
const min = v.min
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
|
||||
return String(value).length >= min ? true : `Minimaal ${min} tekens.`
|
||||
})
|
||||
}
|
||||
if (typeof v.max === 'number') {
|
||||
const max = v.max
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
|
||||
return String(value).length <= max ? true : `Maximaal ${max} tekens.`
|
||||
})
|
||||
}
|
||||
if (typeof v.pattern === 'string' && v.pattern.length > 0) {
|
||||
const pattern = v.pattern
|
||||
rules.push(value => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
const ok = regexValidator(value, pattern)
|
||||
|
||||
return ok === true ? true : 'Ongeldige invoer.'
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case FormFieldType.MULTISELECT:
|
||||
case FormFieldType.CHECKBOX_LIST:
|
||||
if (typeof v.min_selections === 'number') {
|
||||
const min = v.min_selections
|
||||
rules.push(value => {
|
||||
const arr = Array.isArray(value) ? value : []
|
||||
|
||||
return arr.length >= min ? true : `Kies er minimaal ${min}.`
|
||||
})
|
||||
}
|
||||
if (typeof v.max_selections === 'number') {
|
||||
const max = v.max_selections
|
||||
rules.push(value => {
|
||||
const arr = Array.isArray(value) ? value : []
|
||||
|
||||
return arr.length <= max ? true : `Kies er maximaal ${max}.`
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
export function runValidators(rules: Validator[], value: unknown): string | true {
|
||||
for (const rule of rules) {
|
||||
const r = rule(value)
|
||||
if (r !== true) return r
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function isFieldValueEmpty(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return true
|
||||
if (typeof value === 'string') return value.trim() === ''
|
||||
if (Array.isArray(value)) return value.length === 0
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user