import { useState, useCallback, useRef } from "react"; import Joi from "joi"; import { GeneralIdRef } from "../../utils/GeneralIdRef"; import { CustomField, numberParams, textParams, } from "../../modules/manager/customfields/services/customFieldsService"; import { CustomFieldValue, CustomFieldValues, Glossary, } from "../../modules/manager/glossary/services/glossaryService"; import { InputType } from "./Input"; export interface FormError { [key: string]: string; } export interface FormData { [key: string]: | string | number | boolean | CustomFieldValue[] | GeneralIdRef | CustomField[] | bigint | Glossary | null | undefined; } export interface businessValidationError { path: string; message: string; } export interface businessValidationResult { details: businessValidationError[]; } export interface joiSchema { [key: string]: object; } export interface FormState { loaded: boolean; data: FormData; customFields?: CustomField[]; errors: FormError; redirect?: string; } interface UseFormReturn { state: FormState; schema: joiSchema; validate: (data: FormData) => FormError; GetCustomFieldValues: (customField: CustomField) => CustomFieldValue[]; CustomFieldValues: () => CustomFieldValues[]; setCustomFieldValues: ( data: object, customFieldValues: CustomFieldValues[], customFields: CustomField[], ) => void; getCustomFieldType: ( field: CustomFieldValues, childCustomFieldDefinition: CustomField[], ) => string; handleSubmit: ( e: React.FormEvent, doSubmit: (buttonName: string) => Promise, ) => void; handleGeneralError: (ex: any) => void; handleChange: (e: React.ChangeEvent) => void; handleTextAreaChange: (e: React.ChangeEvent) => void; handleCustomFieldChange: (e: React.ChangeEvent) => void; handleTemplateEditorChange: (name: string, value: string) => void; handleSelectChange: (e: React.ChangeEvent) => void; handlePickerChange: (name: string, value: GeneralIdRef) => void; handleDomainPickerChange: (name: string, values: CustomFieldValue[]) => void; handleGlossaryPickerChange: ( name: string, values: CustomFieldValue[], ) => void; handleTemplateFormPickerChange: (name: string, value: GeneralIdRef) => void; handleUserPickerChange: (name: string, value: GeneralIdRef) => void; handleSsoProviderPickerChange: (name: string, value: GeneralIdRef) => void; handleToggleChange: (e: React.ChangeEvent) => void; setState: (updates: Partial) => void; } export const useForm = (initialState: FormState): UseFormReturn => { const [state, setStateInternal] = useState(initialState); const schemaRef = useRef({}); const setState = useCallback((updates: Partial) => { setStateInternal((prev) => ({ ...prev, ...updates })); }, []); const validate = useCallback( (data: FormData): FormError => { let options: Joi.ValidationOptions = { context: {}, abortEarly: false, }; const customFields = state.customFields; let validationSchema = schemaRef.current; if (customFields !== undefined) { for (const customfield of customFields) { const name = "customfield_" + customfield.id; switch (customfield.fieldType) { case "Number": if (customfield.parameters !== undefined) { const parameters: numberParams = JSON.parse( customfield.parameters!, ); options.context![name + "_minEntries"] = customfield.minEntries; if (parameters.minValue) options.context![name + "_minValue"] = Number( parameters.minValue, ); if (parameters.maxValue) options.context![name + "_maxValue"] = Number( parameters.maxValue, ); let minCheck = options.context![name + "_minValue"] ? Joi.number() .empty("") .min(options.context![name + "_minValue"]) : Joi.number().empty(""); let maxCheck = options.context![name + "_maxValue"] ? Joi.number() .empty("") .max(options.context![name + "_maxValue"]) : Joi.number().empty(""); validationSchema[name] = Joi.array() .min(1) .items( Joi.object({ displayValue: Joi.string().allow(""), value: Joi.when("$" + name + "_minEntries", { is: 0, then: Joi.number().empty(""), otherwise: Joi.number().required(), }) .when("$" + name + "_minValue", { is: Joi.number(), then: minCheck, }) .when("$" + name + "_maxValue", { is: Joi.number(), then: maxCheck, }) .label(customfield.name), }), ); } else { validationSchema[name] = Joi.optional().label(customfield.name); } break; default: validationSchema[name] = Joi.optional().label(customfield.name); } } } const joiSchema = Joi.object(validationSchema); const { error } = joiSchema.validate(data, options); let errors: FormError = {}; if (error) { if (error.details === undefined) { errors[error.name] = error.message; } else { for (let item of error.details) { errors[item.path[0]] = item.message; } } } return errors; }, [state.customFields], ); const GetCustomFieldValues = useCallback( (customField: CustomField): CustomFieldValue[] => { const name = "customfield_" + customField.id; const codedValue = state.data[name]; let values: CustomFieldValue[] = []; switch (customField.fieldType) { case "FormTemplate": if (codedValue !== undefined) { const formTemplateValue = { value: JSON.stringify(codedValue as GeneralIdRef), }; values.push(formTemplateValue); } break; case "Sequence": if (codedValue !== undefined) { values = codedValue as CustomFieldValue[]; } break; case "Glossary": if (codedValue !== undefined) { values = codedValue as CustomFieldValue[]; } break; case "Domain": if (codedValue !== undefined) { values = codedValue as CustomFieldValue[]; } break; case "Text": const textParameters: textParams = JSON.parse( customField.parameters!, ); if (textParameters.multiLine) { const textValue = { value: codedValue === undefined ? customField.defaultValue : codedValue, displayValue: codedValue === undefined ? customField.defaultValue : codedValue, } as CustomFieldValue; values.push(textValue); } else { if (codedValue === undefined) { const numberValue = { value: customField.defaultValue, displayValue: customField.defaultValue, } as CustomFieldValue; values.push(numberValue); } else { values = codedValue as CustomFieldValue[]; } } break; case "Number": if (codedValue === undefined) { const numberValue = { value: customField.defaultValue, displayValue: customField.defaultValue, } as CustomFieldValue; values.push(numberValue); } else { values = codedValue as CustomFieldValue[]; } break; default: const textValue = { value: codedValue === undefined ? customField.defaultValue : String((codedValue as CustomFieldValue[])[0].displayValue), }; values.push(textValue); break; } return values; }, [state.data], ); const CustomFieldValues = useCallback((): CustomFieldValues[] => { const customFields = state.customFields; let result: CustomFieldValues[] = []; if (customFields === undefined) { return result; } for (const customfield of customFields) { const values = GetCustomFieldValues(customfield); const id: GeneralIdRef = { id: customfield.id, guid: customfield.guid, }; const newItem: CustomFieldValues = { id, values, }; result.push(newItem); } return result; }, [state.customFields, GetCustomFieldValues]); const getCustomFieldType = useCallback( ( field: CustomFieldValues, childCustomFieldDefinition: CustomField[], ): string => { const fieldDefinition = childCustomFieldDefinition.filter( (x) => x.id === field.id.id, )[0]; if (fieldDefinition.parameters) { const textParameters: textParams = JSON.parse( fieldDefinition.parameters!, ); if (textParameters.multiLine) return "multilinetext"; } return fieldDefinition.fieldType; }, [], ); const setCustomFieldValues = useCallback( ( data: object, customFieldValues: CustomFieldValues[], customFields: CustomField[], ) => { if (customFieldValues !== undefined) { for (const x of customFieldValues) { const customfieldName = "customfield_" + x.id.id; switch (getCustomFieldType(x, customFields).toLowerCase()) { case "glossary": case "domain": case "number": case "text": (data as any)[customfieldName] = x.values.map((x) => { return { displayValue: x.displayValue, value: x.value, }; }); break; case "formtemplate": case "multilinetext": (data as any)[customfieldName] = x.values[0].value; break; default: (data as any)[customfieldName] = x.values; break; } } } }, [getCustomFieldType], ); const handleSubmit = useCallback( ( e: React.FormEvent, doSubmit: (buttonName: string) => Promise, ) => { e.preventDefault(); const submitEvent = e.nativeEvent as SubmitEvent; const submitter = submitEvent.submitter as any; const errors = validate(state.data); setState({ errors }); const disabled = Object.keys(errors).length > 0; if (disabled) return; void doSubmit(submitter.name); }, [state.data, validate, setState], ); const handleGeneralError = useCallback( (ex: any) => { const errors: FormError = { ...state.errors }; if (ex.response) { errors._general = ex.response.data.detail; } else { errors._general = ex.message; } setState({ errors }); }, [state.errors, setState], ); const handleChange = useCallback( (e: React.ChangeEvent) => { const input = e.currentTarget; const data: FormData = { ...state.data }; if ((input as any).type === InputType.checkbox) { data[input.name] = !data[input.name]; } else data[input.name] = input.value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleTextAreaChange = useCallback( (e: React.ChangeEvent) => { const input = e.currentTarget; const data: FormData = { ...state.data }; data[input.name] = input.value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleCustomFieldChange = useCallback( (e: React.ChangeEvent) => { const input = e.currentTarget; const data: FormData = { ...state.data }; switch ((input as any).type) { case InputType.checkbox: data[input.name] = !data[input.name]; break; default: const customFieldValue: CustomFieldValue = { displayValue: input.value, value: input.value, }; data[input.name] = [customFieldValue]; break; } const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleTemplateEditorChange = useCallback( (name: string, value: string) => { const data: FormData = { ...state.data }; data[name] = value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleSelectChange = useCallback( (e: React.ChangeEvent) => { const input = e.currentTarget; const data: FormData = { ...state.data }; data[input.name] = input.value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handlePickerChange = useCallback( (name: string, value: GeneralIdRef) => { const data: FormData = { ...state.data }; data[name] = value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleDomainPickerChange = useCallback( (name: string, values: CustomFieldValue[]) => { const data: FormData = { ...state.data }; data[name] = values; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleGlossaryPickerChange = useCallback( (name: string, values: CustomFieldValue[]) => { const data: FormData = { ...state.data }; data[name] = values; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleTemplateFormPickerChange = useCallback( (name: string, value: GeneralIdRef) => { const data: FormData = { ...state.data }; data[name] = value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleUserPickerChange = useCallback( (name: string, value: GeneralIdRef) => { const data: FormData = { ...state.data }; data[name] = value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleSsoProviderPickerChange = useCallback( (name: string, value: GeneralIdRef) => { const data: FormData = { ...state.data }; data[name] = value; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const handleToggleChange = useCallback( (e: React.ChangeEvent) => { const input = e.currentTarget; const { name, checked } = input; const data: FormData = { ...state.data }; data[name] = checked; const errors = validate(data); setState({ data, errors }); }, [state.data, validate, setState], ); const api: any = { state, schema: schemaRef.current, validate, GetCustomFieldValues, CustomFieldValues, setCustomFieldValues, getCustomFieldType, handleSubmit, handleGeneralError, handleChange, handleTextAreaChange, handleCustomFieldChange, handleTemplateEditorChange, handleSelectChange, handlePickerChange, handleDomainPickerChange, handleGlossaryPickerChange, handleTemplateFormPickerChange, handleUserPickerChange, handleSsoProviderPickerChange, handleToggleChange, setState, }; Object.defineProperty(api, "schema", { get: () => schemaRef.current, set: (value: joiSchema) => { schemaRef.current = value || {}; }, }); return api as UseFormReturn; };