PluginProbe
The Innovative Form Builder – IvyForms / 1.1.1
The Innovative Form Builder – IvyForms v1.1.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / composables / useValidateBeforeSubmit.ts

useValidateBeforeSubmit.ts in The Innovative Form Builder – IvyForms 1.1.1, at frontend/src/composables/useValidateBeforeSubmit.ts

248 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { ref, type Ref } from 'vue'
2 import type { Field } from '@/types/field'
3 import { useApiClient } from '@/composables/useApiClient'
4
5 export type DuplicateCheckState = {
6 value: string
7 passed: boolean
8 }
9
10 export type ValidateBeforeSubmitResult = {
11 valid: boolean
12 is_duplicate?: boolean
13 duplicate_errors?: Record<string, boolean>
14 }
15
16 type FormValues = Record<string, string | number | null | undefined>
17
18 export const getFieldValueKey = (field: Field): string => `${field.type}_${field.fieldIndex}`
19
20 export const processWebsiteFieldValues = (fields: Field[], values: FormValues): FormValues => {
21 const processed = { ...values }
22
23 fields.forEach((field) => {
24 if (field.type !== 'website') {
25 return
26 }
27
28 const key = getFieldValueKey(field)
29 const fullValue = processed[key] || ''
30 const prefix = field.inputPrefix || ''
31 const suffix = field.inputSuffix || ''
32 let userInput = String(fullValue)
33
34 if (fullValue && (prefix || suffix)) {
35 if (prefix && !userInput.startsWith(prefix)) {
36 userInput = prefix + userInput
37 }
38 if (suffix && !userInput.endsWith(suffix)) {
39 userInput = userInput + suffix
40 }
41 }
42
43 processed[key] = userInput
44 })
45
46 return processed
47 }
48
49 const shouldSkipDuplicateCheck = (
50 stateByKey: Record<string, DuplicateCheckState>,
51 fieldKey: string,
52 currentValue: string,
53 ): boolean => {
54 const state = stateByKey[fieldKey]
55 return state !== undefined && state.value === currentValue && state.passed
56 }
57
58 export function buildDuplicateCheckValues(
59 fields: Field[],
60 formValues: FormValues,
61 stateByKey: Record<string, DuplicateCheckState>,
62 options?: { bypassCache?: boolean },
63 ): { values: FormValues; fieldsToCheck: Field[] } {
64 const values: FormValues = {}
65 const fieldsToCheck: Field[] = []
66
67 for (const field of fields) {
68 if (!field.noDuplicates) {
69 continue
70 }
71
72 const fieldKey = getFieldValueKey(field)
73 const rawValue = formValues[fieldKey]
74 if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') {
75 continue
76 }
77
78 const currentValue = String(rawValue)
79 if (!options?.bypassCache && shouldSkipDuplicateCheck(stateByKey, fieldKey, currentValue)) {
80 continue
81 }
82
83 values[fieldKey] = rawValue
84 fieldsToCheck.push(field)
85 }
86
87 return { values, fieldsToCheck }
88 }
89
90 export async function callValidateBeforeSubmit(
91 request: ReturnType<typeof useApiClient>['request'],
92 params: {
93 formId: string | number
94 values: FormValues
95 nonce: string
96 pageId?: string
97 },
98 ): Promise<{ result: ValidateBeforeSubmitResult | null; error: unknown }> {
99 const { data, error, status } = await request('form/validate_before_submit/', {
100 method: 'POST',
101 headers: {
102 'Content-Type': 'application/json',
103 },
104 data: {
105 formId: params.formId,
106 values: params.values,
107 nonce: params.nonce,
108 ...(params.pageId ? { pageId: params.pageId } : {}),
109 },
110 })
111
112 if (error || status !== 200 || !data) {
113 return { result: null, error: error || data }
114 }
115
116 const payload = (data as { data?: { data?: ValidateBeforeSubmitResult } }).data?.data
117 if (!payload) {
118 return { result: null, error: 'Invalid validate_before_submit response' }
119 }
120
121 return { result: payload, error: null }
122 }
123
124 export function useValidateBeforeSubmit(options: {
125 request: ReturnType<typeof useApiClient>['request']
126 getLabel: (key: string) => string
127 formData: Ref<{ id: string | number }>
128 formFields: Ref<Field[]>
129 formValues: Ref<FormValues>
130 formNonce: Ref<string | null>
131 fieldErrors: Ref<Record<string, string>>
132 getPageFields: (pageId: string) => Field[]
133 }) {
134 const duplicateCheckState = ref<Record<string, DuplicateCheckState>>({})
135
136 const applyDuplicateErrors = (duplicateFields: Record<string, boolean>) => {
137 for (const field of options.formFields.value) {
138 const fieldId = String(field.id)
139 if (!duplicateFields[field.id] && !duplicateFields[fieldId]) {
140 continue
141 }
142
143 const fieldKey = getFieldValueKey(field)
144 const fieldValue = options.formValues.value[fieldKey]
145 if (!fieldValue || fieldValue.toString().trim() === '') {
146 continue
147 }
148
149 options.fieldErrors.value[fieldKey] = options
150 .getLabel('duplicate_value_error_label')
151 .replace('{label}', field.label?.toLowerCase() || 'value')
152 }
153 }
154
155 const recordDuplicateCheckResults = (fieldsToCheck: Field[], passed: boolean) => {
156 for (const field of fieldsToCheck) {
157 const fieldKey = getFieldValueKey(field)
158 const rawValue = options.formValues.value[fieldKey]
159 if (rawValue === undefined || rawValue === null) {
160 continue
161 }
162 duplicateCheckState.value[fieldKey] = {
163 value: String(rawValue),
164 passed,
165 }
166 }
167 }
168
169 const validateDuplicatesBeforeProceed = async (
170 pageId?: string,
171 checkOptions?: { bypassCache?: boolean },
172 ): Promise<'ok' | 'duplicate' | 'request_failed'> => {
173 const scopeFields = pageId
174 ? options.getPageFields(pageId).filter((field) => field.noDuplicates)
175 : options.formFields.value.filter((field) => field.noDuplicates)
176
177 if (scopeFields.length === 0) {
178 return 'ok'
179 }
180
181 const { values, fieldsToCheck } = buildDuplicateCheckValues(
182 scopeFields,
183 options.formValues.value,
184 duplicateCheckState.value,
185 checkOptions,
186 )
187
188 if (fieldsToCheck.length === 0) {
189 return 'ok'
190 }
191
192 const processedValues = processWebsiteFieldValues(scopeFields, values)
193
194 const nonce = options.formNonce.value
195 if (!nonce) {
196 return 'request_failed'
197 }
198
199 const { result, error } = await callValidateBeforeSubmit(options.request, {
200 formId: options.formData.value.id,
201 values: processedValues,
202 nonce,
203 pageId,
204 })
205
206 if (!result) {
207 console.error('validate_before_submit failed:', error)
208 return 'request_failed'
209 }
210
211 if (result.valid) {
212 recordDuplicateCheckResults(fieldsToCheck, true)
213 return 'ok'
214 }
215
216 recordDuplicateCheckResults(fieldsToCheck, false)
217 if (result.duplicate_errors) {
218 applyDuplicateErrors(result.duplicate_errors)
219 }
220 return 'duplicate'
221 }
222
223 return {
224 duplicateCheckState,
225 validateDuplicatesBeforeProceed,
226 applyDuplicateErrors,
227 }
228 }
229
230 /**
231 * Standalone client for integrations (e.g. Amelia booking flow).
232 */
233 export async function validateBeforeSubmitForIntegration(params: {
234 formId: string | number
235 values: FormValues
236 nonce: string
237 pageId?: string
238 }): Promise<ValidateBeforeSubmitResult> {
239 const { request } = useApiClient()
240 const { result, error } = await callValidateBeforeSubmit(request, params)
241
242 if (!result) {
243 throw error instanceof Error ? error : new Error(String(error))
244 }
245
246 return result
247 }
248