589 lines
17 KiB
TypeScript
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;
|
|
};
|