webui/src/components/common/useForm.ts

589 lines
17 KiB
TypeScript

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<HTMLFormElement>,
doSubmit: (buttonName: string) => Promise<void>,
) => void;
handleGeneralError: (ex: any) => void;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleTextAreaChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleCustomFieldChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleTemplateEditorChange: (name: string, value: string) => void;
handleSelectChange: (e: React.ChangeEvent<HTMLSelectElement>) => 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<HTMLInputElement>) => void;
setState: (updates: Partial<FormState>) => void;
}
export const useForm = (initialState: FormState): UseFormReturn => {
const [state, setStateInternal] = useState<FormState>(initialState);
const schemaRef = useRef<joiSchema>({});
const setState = useCallback((updates: Partial<FormState>) => {
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<HTMLFormElement>,
doSubmit: (buttonName: string) => Promise<void>,
) => {
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<HTMLInputElement>) => {
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<HTMLTextAreaElement>) => {
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<HTMLInputElement>) => {
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<HTMLSelectElement>) => {
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<HTMLInputElement>) => {
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;
};