Form.tsx is gone, useForm hook is the replacement,

This commit is contained in:
Colin Dawson 2026-01-31 17:56:57 +00:00
parent dce95c8345
commit 83cf83b6fc
5 changed files with 741 additions and 1480 deletions

View File

@ -1,957 +0,0 @@
import React from "react";
import Joi from "joi";
import Input, { InputType } from "./Input";
import ToggleSlider from "./ToggleSlider";
import Select from "./Select";
import Option from "./option";
import { GeneralIdRef } from "../../utils/GeneralIdRef";
import SequencePicker from "../pickers/SequencePicker";
import GlossaryPicker from "../pickers/GlossaryPicker";
import CustomFieldsEditor, { CustomFieldEditorAdd, CustomFieldEditorDelete } from "./CustomFieldsEditor";
import { CustomField, numberParams, textParams } from "../../modules/manager/customfields/services/customFieldsService";
import FormTemplatePicker from "../pickers/FormTemplatePicker";
import { CustomFieldValue, CustomFieldValues, Glossary } from "../../modules/manager/glossary/services/glossaryService";
import TemplateEditor from "./TemplateEditor";
import DomainPicker from "../pickers/DomainPicker";
import UserPicker from "../pickers/UserPicker";
import Button, { ButtonType } from "./Button";
import Expando from "./expando";
import ErrorBlock from "./ErrorBlock";
import SsoProviderPicker from "../pickers/SsoProviderPicker";
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 propertyValue {
name: string;
value: string | boolean | number;
}
export interface joiSchema {
[key: string]: object;
}
export interface FormState {
loaded: boolean;
data: FormData;
customFields?: CustomField[];
errors: FormError;
redirect?: string;
}
export interface Match<P> {
params: P;
isExact: boolean;
path: string;
url: string;
}
export interface State {
from: Location;
}
export interface LocationProps {
hash: string;
pathname: string;
search: string;
state: State;
}
export interface FormProps<P> {
location: LocationProps;
match: Match<P>;
staticContext?: any;
}
class Form<P, FP extends FormProps<P>, FS extends FormState> extends React.Component<FP, FS> {
schema: joiSchema = {};
validate = (data: FormData) => {
let options: Joi.ValidationOptions = {
context: {},
abortEarly: false,
};
const { customFields } = this.state;
let schema = this.schema;
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("");
schema[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 {
schema[name] = Joi.optional().label(customfield.name);
}
break;
default:
schema[name] = Joi.optional().label(customfield.name);
}
}
}
const joiSchema = Joi.object(schema);
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;
};
GetCustomFieldValues = (customField: CustomField) => {
const name = "customfield_" + customField.id;
const { data } = this.state;
const codedValue = 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;
};
CustomFieldValues = () => {
const { customFields } = this.state;
let result: CustomFieldValues[] = [];
if (customFields === undefined) {
return result;
}
for (const customfield of customFields) {
const values = this.GetCustomFieldValues(customfield);
const id: GeneralIdRef = {
id: customfield.id,
guid: customfield.guid,
};
const newItem: CustomFieldValues = {
id,
values,
};
result.push(newItem);
}
return result;
};
setCustomFieldValues(data: object, customFieldValues: CustomFieldValues[], customFields: CustomField[]) {
if (customFieldValues !== undefined) {
for (const x of customFieldValues) {
const customfieldName = "customfield_" + x.id.id;
switch (this.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 = (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;
};
handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const submitEvent = e.nativeEvent as SubmitEvent;
const submitter = submitEvent.submitter as any;
const errors = this.validate(this.state.data);
this.setState({ errors: errors });
const disabled = Object.keys(errors).length > 0;
if (disabled) return;
this.doSubmit(submitter.name);
};
doSubmit = async (buttonName: string) => {};
handleGeneralError = (ex: any) => {
const errors: FormError = { ...this.state.errors };
if (ex.response) {
errors._general = ex.response.data.detail;
} else {
errors._general = ex.message;
}
this.setState({ errors });
};
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const data: FormData = { ...this.state.data };
if ((input as any).type === InputType.checkbox) {
data[input.name] = !data[input.name];
} else data[input.name] = input.value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const input = e.currentTarget;
const data: FormData = { ...this.state.data };
data[input.name] = input.value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleCustomFieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const data: FormData = { ...this.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 = this.validate(data);
this.setState({ data, errors });
};
handleTemplateEditorChange = (name: string, value: string) => {
const data: FormData = { ...this.state.data };
data[name] = value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget;
const data: FormData = { ...this.state.data };
data[input.name] = input.value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handlePickerChange = (name: string, value: GeneralIdRef) => {
const data: FormData = { ...this.state.data };
data[name] = value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleDomainPickerChange = (name: string, values: CustomFieldValue[]) => {
const data: FormData = { ...this.state.data };
data[name] = values;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleGlossaryPickerChange = (name: string, values: CustomFieldValue[]) => {
const data: FormData = { ...this.state.data };
data[name] = values;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleTemplateFormPickerChange = (name: string, value: GeneralIdRef) => {
const data: FormData = { ...this.state.data };
data[name] = value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleUserPickerChange = (name: string, value: GeneralIdRef) => {
const data: FormData = { ...this.state.data };
data[name] = value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleSsoProviderPickerChange = (name: string, value: GeneralIdRef) => {
const data: FormData = { ...this.state.data };
data[name] = value;
const errors = this.validate(data);
this.setState({ data, errors });
};
handleToggleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const { name, checked } = input;
const data: FormData = { ...this.state.data };
data[name] = checked;
const errors = this.validate(data);
this.setState({ data, errors });
};
renderButton(
label: string,
name?: string,
onClick?: (keyValue: any) => {},
testid?: string,
enabled: boolean = true,
buttonType: ButtonType = ButtonType.primary,
overrideErrorChecking: boolean = false
) {
const { errors } = this.state;
let disabled = !enabled || Object.keys(errors).filter((x) => !x.startsWith("_")).length > 0;
if (overrideErrorChecking) disabled = !enabled;
return (
<Button testid={testid} disabled={disabled} name={name ?? label} buttonType={buttonType} onClick={onClick}>
{label}
</Button>
);
}
renderError(name: string) {
const { errors } = this.state;
return <ErrorBlock error={errors[name]} />;
}
renderInput(
name: string,
label: string,
type: InputType = InputType.text,
readOnly = false,
defaultValue: string = "",
placeHolder: string = "",
maxLength: number = 0,
visible: boolean = true,
autoComplete: string | undefined = undefined
) {
const { data, errors } = this.state;
let value = data[name];
let cleanValue: string | undefined;
if (value === undefined) {
cleanValue = defaultValue;
} else if (typeof value === "string") {
cleanValue = value as string;
} else if (typeof value === "number") {
cleanValue = String(value);
} else if (typeof value === "boolean") {
cleanValue = String(value);
} else if (value as CustomFieldValue) {
cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value.toString();
}
if (readOnly) {
return (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
readOnly
placeHolder={placeHolder}
hidden={!visible}
autoComplete={autoComplete}
/>
);
} else {
return (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
onChange={this.handleChange}
placeHolder={placeHolder}
hidden={!visible}
autoComplete={autoComplete}
/>
);
}
}
renderInputWithChangeEvent(
name: string,
label: string,
type: InputType = InputType.text,
readOnly = false,
handleChangeEvent: any,
defaultValue: string = "",
placeHolder: string = "",
maxLength: number = 0
) {
const { data, errors } = this.state;
let value = data[name];
let cleanValue: string | undefined;
if (value === undefined) {
cleanValue = defaultValue;
} else if (typeof value === "string") {
cleanValue = value as string;
} else if (typeof value === "number") {
cleanValue = String(value);
} else if (typeof value === "boolean") {
cleanValue = String(value);
} else if (value as CustomFieldValue) {
cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value.toString();
}
if (readOnly) {
return (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
readOnly
placeHolder={placeHolder}
/>
);
} else {
return (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
onChange={handleChangeEvent}
placeHolder={placeHolder}
/>
);
}
}
renderInputNumber(name: string, label: string, readOnly = false, defaultValue: string = "", min?: number, max?: number, step: number = 1) {
const { data, errors } = this.state;
let value = data[name];
let cleanValue: string | undefined;
if (value === undefined) {
cleanValue = defaultValue;
} else if (typeof value === "string") {
cleanValue = value as string;
} else if (typeof value === "number") {
cleanValue = String(value);
} else if (typeof value === "boolean") {
cleanValue = String(value);
} else if (value as CustomFieldValue) {
cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value.toString();
}
if (readOnly) {
return <Input includeLabel={true} type={InputType.number} name={name} label={label} value={cleanValue} error={errors[name]} readOnly />;
} else {
return (
<Input
includeLabel={true}
type={InputType.number}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
min={min}
max={max}
step={step}
onChange={this.handleChange}
/>
);
}
}
renderInputTextarea(includeLabel: boolean, name: string, label: string, readOnly = false, defaultValue: string = "") {
const { data, errors } = this.state;
let value = data[name];
let cleanValue: string | undefined;
if (value === undefined) {
cleanValue = defaultValue;
} else if (typeof value === "string") {
cleanValue = value as string;
}
if (readOnly) {
return (
<Input
includeLabel={includeLabel}
type={InputType.textarea}
key={name}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
readOnly
/>
);
} else {
return (
<Input
includeLabel={includeLabel}
type={InputType.textarea}
key={name}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
onTextAreaChange={this.handleTextAreaChange}
/>
);
}
}
renderCustomFieldInput(includeLabel: boolean, name: string, label: string, type: InputType = InputType.text, readOnly = false, defaultValue: string = "") {
const { data, errors } = this.state;
let value = data[name];
let cleanValue: string | undefined;
if (value === undefined) {
cleanValue = defaultValue;
} else if (typeof value === "string") {
cleanValue = value as string;
} else if (typeof value === "number") {
cleanValue = String(value);
} else if (typeof value === "boolean") {
cleanValue = String(value);
} else if (value as CustomFieldValue) {
const customFieldValue = value as CustomFieldValue[];
cleanValue = customFieldValue[0].displayValue ?? customFieldValue[0].value?.toString();
}
if (readOnly) {
return <Input includeLabel={includeLabel} type={type} name={name} label={label} value={cleanValue} error={errors[name]} readOnly />;
} else {
return (
<Input
includeLabel={includeLabel}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
onChange={this.handleCustomFieldChange}
/>
);
}
}
renderCustomFieldNumber(
includeLabel: boolean,
name: string,
label: string,
readOnly = false,
defaultValue: string = "",
min?: number,
max?: number,
step: number = 1
) {
const { data, errors } = this.state;
let values: CustomFieldValue[] = data[name] as CustomFieldValue[];
let value: CustomFieldValue | undefined = undefined;
if (values) {
if (values.length > 0) {
value = values[0];
}
}
let cleanValue: string | undefined;
if (value === undefined) {
cleanValue = defaultValue;
} else if (typeof value === "string") {
cleanValue = value as string;
} else if (typeof value === "number") {
cleanValue = String(value);
} else if (value as CustomFieldValue) {
cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value?.toString();
}
if (readOnly) {
return <Input includeLabel={includeLabel} type={InputType.number} name={name} label={label} value={cleanValue} error={errors[name]} readOnly />;
} else {
return (
<Input
includeLabel={includeLabel}
type={InputType.number}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
min={min}
max={max}
step={step}
onChange={this.handleCustomFieldChange}
/>
);
}
}
renderTemplateEditor(className: string, name: string, label: string, allowCustomFields: boolean) {
const { data } = this.state;
let value = data[name] as string;
return (
<div>
<label htmlFor={name}>{label}</label>
<TemplateEditor className={className} name={name} data={value} onChange={this.handleTemplateEditorChange} showFields={allowCustomFields} />
</div>
);
}
renderSelect(name: string, label: string, options: Option[]) {
const { data, errors } = this.state;
return <Select name={name} label={label} value={data[name]} options={options} error={errors[name]} onChange={this.handleSelectChange} />;
}
renderToggle(name: string, label: string) {
const { data, errors } = this.state;
return <ToggleSlider name={name} label={label} defaultChecked={Boolean(data[name])} error={errors[name]} onChange={this.handleToggleChange} />;
}
renderSequencePicker(includeLabel: boolean, name: string, label: string) {
const { data, errors } = this.state;
return (
<SequencePicker includeLabel={includeLabel} name={name} label={label} value={data[name]} error={errors[name]} onChange={this.handlePickerChange} />
);
}
renderGlossaryPicker(includeLabel: boolean, name: string, label: string, maxEntries?: number, refElementId?: GeneralIdRef) {
const { data, errors } = this.state;
const glossaryValues: CustomFieldValue[] | undefined = data[name] as any as CustomFieldValue[];
return (
<GlossaryPicker
includeLabel={includeLabel}
name={name}
label={label}
maxEntries={maxEntries}
values={glossaryValues}
error={errors[name]}
rootItem={refElementId}
onChange={this.handleGlossaryPickerChange}
/>
);
}
renderDomainPicker(includeLabel: boolean, name: string, label: string, minEntries: number, maxEntries?: number) {
const { data, errors } = this.state;
const domainValues: CustomFieldValue[] | undefined = data[name] as any as CustomFieldValue[];
return (
<DomainPicker
includeLabel={includeLabel}
name={name}
label={label}
minEntries={minEntries}
maxEntries={maxEntries}
values={domainValues}
error={errors[name]}
onChange={this.handleDomainPickerChange}
/>
);
}
renderTemplatePicker(includeLabel: boolean, name: string, label: string) {
const { data, errors } = this.state;
const templateValue: GeneralIdRef = data[name] as any as GeneralIdRef;
return (
<FormTemplatePicker
includeLabel={includeLabel}
name={name}
label={label}
value={templateValue}
error={errors[name]}
onChange={this.handleTemplateFormPickerChange}
/>
);
}
renderUserPicker(name: string, label: string) {
const { data, errors } = this.state;
const glossaryValue: GeneralIdRef | undefined = data[name] as any as GeneralIdRef;
return <UserPicker name={name} label={label} value={glossaryValue} error={errors[name]} onChange={this.handleUserPickerChange} />;
}
renderSsoProviderPicker(name: string, label: string) {
const { data, errors } = this.state;
const glossaryValue: GeneralIdRef | undefined = data[name] as any as GeneralIdRef;
return <SsoProviderPicker name={name} label={label} value={glossaryValue} error={errors[name]} onChange={this.handleSsoProviderPickerChange} />;
}
renderCustomFieldsEditor(name: string, label: string, selected: CustomField[], onAdd: CustomFieldEditorAdd, onDelete: CustomFieldEditorDelete) {
const { data, errors } = this.state;
return (
<CustomFieldsEditor name={name} label={label} value={data[name] as []} error={errors[name]} exclude={selected} onAdd={onAdd} onDelete={onDelete} />
);
}
renderCustomFields(customFields: CustomField[] | undefined) {
if (customFields === undefined) return <></>;
let customFieldsBlock: JSX.Element[] = [];
for (const customField of customFields) customFieldsBlock.push(this.renderCustomField(customField, true));
return <>{customFieldsBlock.map((x) => x)}</>;
}
renderCustomField(customField: CustomField, includeLabel: boolean) {
switch (customField.fieldType.toLowerCase()) {
case "text":
const textParameters: textParams = JSON.parse(customField.parameters!);
if (textParameters.multiLine) {
return this.renderInputTextarea(includeLabel, "customfield_" + customField.id, customField.name, false, customField.defaultValue);
} else {
return this.renderCustomFieldInput(
includeLabel,
"customfield_" + customField.id,
customField.name,
InputType.text,
false,
customField.defaultValue
);
}
case "sequence":
return this.renderCustomFieldInput(includeLabel, "customfield_" + customField.id, customField.name, InputType.text, true);
case "formtemplate":
return this.renderTemplatePicker(includeLabel, "customfield_" + customField.id, customField.name);
case "glossary":
return this.renderGlossaryPicker(
includeLabel,
"customfield_" + customField.id,
customField.name,
customField.maxEntries,
customField.refElementId
);
case "number":
const numberParameters: numberParams = JSON.parse(customField.parameters!);
return this.renderCustomFieldNumber(
includeLabel,
"customfield_" + customField.id,
customField.name,
false,
customField.defaultValue,
numberParameters.minValue ? Number(numberParameters.minValue) : undefined,
numberParameters.maxValue ? Number(numberParameters.maxValue) : undefined,
numberParameters.step ? Number(numberParameters.step) : undefined
);
case "domain":
return this.renderDomainPicker(includeLabel, "customfield_" + customField.id, customField.name, customField.minEntries, customField.maxEntries);
default:
return <>{customField.name + " " + customField.fieldType}</>;
}
}
renderDropSection(name: string, title: JSX.Element, content: JSX.Element) {
const { errors } = this.state;
return (
<Expando name={name} title={title} error={errors[name]}>
{content}
</Expando>
);
}
}
export default Form;

View File

@ -1,4 +1,10 @@
import React from "react"; import React, {
useCallback,
useEffect,
useImperativeHandle,
useRef,
forwardRef,
} from "react";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
import formsService, { import formsService, {
CreateFormInstance, CreateFormInstance,
@ -6,79 +12,58 @@ import formsService, {
} from "../../modules/manager/forms/services/formsService"; } from "../../modules/manager/forms/services/formsService";
import parse, { HTMLReactParserOptions, domToReact } from "html-react-parser"; import parse, { HTMLReactParserOptions, domToReact } from "html-react-parser";
import { CustomField } from "../../modules/manager/customfields/services/customFieldsService"; import { CustomField } from "../../modules/manager/customfields/services/customFieldsService";
import Form, { FormState } from "./Form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import Loading from "./Loading"; import Loading from "./Loading";
import { useForm } from "./useForm";
import { renderCustomField } from "./formHelpers";
import { CustomFieldValue } from "../../modules/manager/glossary/services/glossaryService";
interface TemplateFillerProps { interface TemplateFillerProps {
templateId?: GeneralIdRef; templateId?: GeneralIdRef;
formInstanceId?: GeneralIdRef; formInstanceId?: GeneralIdRef;
onValidationChanged?: () => {}; onValidationChanged?: () => void;
} }
interface TemplateFillerState extends FormState { export interface TemplateFillerHandle {
customFields?: CustomField[]; hasValidationErrors: () => boolean;
template: { Save: () => Promise<any>;
}
interface TemplateState {
name?: string; name?: string;
templateId?: GeneralIdRef; templateId?: GeneralIdRef;
version?: bigint; version?: bigint;
definition?: string; definition?: string;
};
} }
class TemplateFiller extends Form< const TemplateFiller = forwardRef<TemplateFillerHandle, TemplateFillerProps>(
TemplateFillerProps, ({ templateId, formInstanceId, onValidationChanged }, ref) => {
any, const form = useForm({
TemplateFillerState
> {
state: TemplateFillerState = {
loaded: false, loaded: false,
customFields: undefined, data: {},
template: { errors: {},
redirect: "",
});
const [template, setTemplate] = React.useState<TemplateState>({
name: undefined, name: undefined,
templateId: undefined, templateId: undefined,
version: undefined, version: undefined,
definition: undefined, definition: undefined,
}, });
data: {},
errors: {},
};
schema = {}; const prevHasErrorsRef = useRef<boolean>(false);
componentDidMount(): void { form.schema = {};
this.loadTemplate();
}
componentDidUpdate( const loadTemplate = useCallback(async () => {
prevProps: Readonly<TemplateFillerProps>,
prevState: Readonly<TemplateFillerState>,
snapshot?: any,
): void {
if (
prevProps.formInstanceId !== this.props.formInstanceId ||
prevProps.templateId !== this.props.templateId
)
this.loadTemplate();
if (this.props.onValidationChanged) {
const prevErrorCount = Object.keys(prevState.errors).length > 0;
const errorCount = Object.keys(this.state.errors).length > 0;
if (prevErrorCount !== errorCount) {
this.props.onValidationChanged();
}
}
}
loadTemplate = async () => {
const { templateId, formInstanceId } = this.props;
let loadedData: any; let loadedData: any;
if (templateId !== undefined) { if (templateId !== undefined) {
loadedData = await formsService.getForm(templateId?.id, templateId?.guid); loadedData = await formsService.getForm(
//Get the form definiton for the template provided by templateId and load. templateId?.id,
templateId?.guid,
);
loadedData.templateId = undefined; loadedData.templateId = undefined;
loadedData.customFieldValues = undefined; loadedData.customFieldValues = undefined;
loadedData.updatedVersion = undefined; loadedData.updatedVersion = undefined;
@ -87,7 +72,6 @@ class TemplateFiller extends Form<
formInstanceId?.id, formInstanceId?.id,
formInstanceId?.guid, formInstanceId?.guid,
); );
console.log("formInstanceId", loadedData);
} else { } else {
loadedData = { loadedData = {
name: undefined, name: undefined,
@ -102,23 +86,81 @@ class TemplateFiller extends Form<
}; };
} }
const { template, data } = this.state; const newTemplate: TemplateState = {
name: loadedData.name,
template.name = loadedData.name; templateId: MakeGeneralIdRef(loadedData.id, loadedData.guid),
template.templateId = MakeGeneralIdRef(loadedData.id, loadedData.guid); version: loadedData.version,
template.version = loadedData.version; definition: loadedData.definition,
template.definition = loadedData.definition;
const customFields = loadedData.customFieldDefinitions;
this.setCustomFieldValues(data, loadedData.customFieldValues, customFields);
this.setState({ loaded: true, template, customFields, data });
}; };
parseDefinition = ( const newCustomFields = loadedData.customFieldDefinitions as
definition: string, | CustomField[]
customFieldDefinitions: CustomField[], | undefined;
) => { const newData = { ...form.state.data } as Record<string, any>;
form.setCustomFieldValues(
newData,
loadedData.customFieldValues,
newCustomFields ?? [],
);
form.setState({
loaded: true,
data: newData,
customFields: newCustomFields,
});
setTemplate(newTemplate);
}, [templateId, formInstanceId, form]);
useEffect(() => {
void loadTemplate();
}, [loadTemplate]);
useEffect(() => {
if (!onValidationChanged) return;
const hasErrors = Object.keys(form.state.errors).length > 0;
if (prevHasErrorsRef.current !== hasErrors) {
prevHasErrorsRef.current = hasErrors;
onValidationChanged();
}
}, [form.state.errors, onValidationChanged]);
const handleCustomFieldChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (e.target instanceof HTMLTextAreaElement) {
form.handleTextAreaChange(
e as React.ChangeEvent<HTMLTextAreaElement>,
);
} else {
form.handleCustomFieldChange(
e as React.ChangeEvent<HTMLInputElement>,
);
}
},
[form],
);
const handleCustomFieldPickerChange = useCallback(
(name: string, value: GeneralIdRef | CustomFieldValue[]) => {
if (Array.isArray(value)) {
form.handleGlossaryPickerChange(name, value);
} else {
form.handlePickerChange(name, value as GeneralIdRef);
}
},
[form],
);
const getCustomFieldType = useCallback(
(field: CustomFieldValue, customFields: CustomField[]) => {
return form.getCustomFieldType(field as any, customFields);
},
[form],
);
const parseDefinition = useCallback(
(definition: string, customFieldDefinitions: CustomField[]) => {
const options: HTMLReactParserOptions = { const options: HTMLReactParserOptions = {
replace: (domNode) => { replace: (domNode) => {
const domNodeAsAny: any = domNode; const domNodeAsAny: any = domNode;
@ -127,7 +169,15 @@ class TemplateFiller extends Form<
const customField = customFieldDefinitions.filter( const customField = customFieldDefinitions.filter(
(x) => x.guid === domNodeAsAny.attribs.guid, (x) => x.guid === domNodeAsAny.attribs.guid,
)[0]; )[0];
return this.renderCustomField(customField, false); return renderCustomField(
customField,
false,
form.state.data,
form.state.errors,
handleCustomFieldChange,
handleCustomFieldPickerChange,
getCustomFieldType,
);
} }
} else if (domNodeAsAny.name === "p") { } else if (domNodeAsAny.name === "p") {
return ( return (
@ -140,60 +190,68 @@ class TemplateFiller extends Form<
}; };
return parse(definition, options); return parse(definition, options);
}; },
[
form.state.data,
form.state.errors,
handleCustomFieldChange,
handleCustomFieldPickerChange,
getCustomFieldType,
],
);
hasValidationErrors = (): boolean => { const hasValidationErrors = useCallback((): boolean => {
const { errors } = this.state; return Object.keys(form.state.errors).length > 0;
}, [form.state.errors]);
const result = Object.keys(errors).length > 0; const Save = useCallback(async () => {
return result; const { errors } = form.state;
};
async Save() {
const { errors } = this.state;
const { templateId, version } = this.state.template;
const { formInstanceId } = this.props;
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
toast.error("There are errors on the form"); toast.error("There are errors on the form");
throw new Error("There are errors on the form"); throw new Error("There are errors on the form");
} }
const customFieldValues = this.CustomFieldValues(); const customFieldValues = form.CustomFieldValues();
if (formInstanceId !== undefined) { if (formInstanceId !== undefined) {
if (templateId === undefined) throw Error("TemplateId cannot be null"); if (template.templateId === undefined)
throw Error("TemplateId cannot be null");
if (version === undefined) throw Error("Version cannot be null"); if (template.version === undefined)
throw Error("Version cannot be null");
const editFormInstance: EditFormInstance = { const editFormInstance: EditFormInstance = {
formInstanceId, formInstanceId,
templateId, templateId: template.templateId,
version, version: template.version,
customFieldValues, customFieldValues,
}; };
await formsService.putFormInstance(editFormInstance); await formsService.putFormInstance(editFormInstance);
} else { } else {
if (templateId !== undefined && version !== undefined) { if (
//const customFieldValues = this.CustomFieldValues(); template.templateId !== undefined &&
template.version !== undefined
) {
const formInstance: CreateFormInstance = { const formInstance: CreateFormInstance = {
templateId, templateId: template.templateId,
version, version: template.version,
customFieldValues, customFieldValues,
}; };
return await formsService.postFormInstance(formInstance); return await formsService.postFormInstance(formInstance);
} else throw new Error("template unknown");
} }
throw new Error("template unknown");
} }
}, [form, template, formInstanceId]);
render() { useImperativeHandle(ref, () => ({
const { loaded, template, customFields } = this.state; hasValidationErrors,
Save,
}));
const { loaded, customFields } = form.state;
let parsedDefinition: any; let parsedDefinition: any;
if (template.definition) if (template.definition && customFields)
parsedDefinition = this.parseDefinition( parsedDefinition = parseDefinition(template.definition, customFields);
template.definition,
customFields!,
);
else parsedDefinition = <></>; else parsedDefinition = <></>;
return ( return (
@ -201,7 +259,9 @@ class TemplateFiller extends Form<
<div className="ck-content form-editor">{parsedDefinition}</div> <div className="ck-content form-editor">{parsedDefinition}</div>
</Loading> </Loading>
); );
} },
} );
TemplateFiller.displayName = "TemplateFiller";
export default TemplateFiller; export default TemplateFiller;

View File

@ -1,33 +1,23 @@
import React, { useCallback, useEffect, useState } from "react";
import { Link, Navigate } from "react-router-dom"; import { Link, Navigate } from "react-router-dom";
import Joi from "joi"; import Joi from "joi";
import Form, { FormState, FormData } from "../../../components/common/Form";
import authentication from "../services/authenticationService"; import authentication from "../services/authenticationService";
import { InputType } from "../../../components/common/Input"; import { InputType } from "../../../components/common/Input";
import { ButtonType } from "../../../components/common/Button"; import { ButtonType } from "../../../components/common/Button";
import { useForm } from "../../../components/common/useForm";
import {
renderButton,
renderError,
renderInput,
} from "../../../components/common/formHelpers";
//import '../../../Sass/login.scss'; const LoginForm: React.FC = () => {
const [isInNextStage, setIsInNextStage] = useState(false);
const [emailSent, setEmailSent] = useState(false);
const passwordMaxLength = 255;
export interface LoginFormStateData extends FormData { const form = useForm({
username: string;
password: string;
tfaNeeded: boolean;
requestTfaRemoval: boolean;
securityCode: string;
}
export interface LoginFormState extends FormState {
passwordMaxLength: number;
isInNextStage: boolean;
emailSent: boolean;
data: LoginFormStateData;
}
class LoginForm extends Form<any, any, LoginFormState> {
state = {
loaded: true, loaded: true,
passwordMaxLength: 255,
isInNextStage: false,
emailSent: false,
data: { data: {
username: "", username: "",
password: "", password: "",
@ -36,9 +26,10 @@ class LoginForm extends Form<any, any, LoginFormState> {
securityCode: "", securityCode: "",
}, },
errors: {}, errors: {},
}; redirect: "",
});
schema = { const schema = {
username: Joi.string() username: Joi.string()
.required() .required()
.email({ tlds: { allow: false } }) .email({ tlds: { allow: false } })
@ -49,46 +40,16 @@ class LoginForm extends Form<any, any, LoginFormState> {
securityCode: Joi.string().allow("").label("Authenticate"), securityCode: Joi.string().allow("").label("Authenticate"),
}; };
doSubmit = async (buttonName: string) => { form.schema = schema;
const { data } = this.state;
await this.performLogin(data);
};
handleNextClick = async (event: React.MouseEvent) => { useEffect(() => {
const data: LoginFormStateData = { ...this.state.data }; window.location.replace("/login");
var validationResult = this.schema.username.validate(data.username); }, []);
if (validationResult.error === undefined) {
const stateData = this.state;
stateData.isInNextStage = true;
this.setState(stateData);
}
};
handleForgetPassword = async () => { const performLogin = useCallback(
async (data: any) => {
try { try {
const stateData = this.state; const result = await authentication.login(
await authentication.forgotPassword(stateData.data.username);
stateData.emailSent = true;
stateData.data.username = "";
stateData.data.password = "";
this.setState(stateData);
} catch (ex: any) {
this.handleGeneralError(ex);
}
};
authenticationWorkAround = async () => {
const data: LoginFormStateData = { ...this.state.data };
data.requestTfaRemoval = true;
await this.performLogin(data);
this.setState({ data });
};
private async performLogin(data: LoginFormStateData) {
try {
let result = await authentication.login(
data.username, data.username,
data.password, data.password,
data.securityCode, data.securityCode,
@ -96,35 +57,65 @@ class LoginForm extends Form<any, any, LoginFormState> {
); );
switch (result) { switch (result) {
case 1: //requires tfa case 1: {
const { data } = this.state; const nextData = { ...form.state.data };
if (data.tfaNeeded === true) { if (!nextData.tfaNeeded) {
//TFA removal Request accepted. nextData.tfaNeeded = true;
} else { form.setState({ data: nextData });
data.tfaNeeded = true;
this.setState({ data });
} }
break; break;
case 2: //logged in }
case 2:
window.location.href = "/"; window.location.href = "/";
break; break;
default: default:
break; //treat at though not logged in. break;
} }
} catch (ex: any) { } catch (ex: any) {
this.handleGeneralError(ex); form.handleGeneralError(ex);
} }
},
[form],
);
const doSubmit = async () => {
const { data } = form.state;
await performLogin(data);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
form.handleSubmit(e, async () => doSubmit());
};
const handleNextClick = async () => {
const data = { ...form.state.data };
const validationResult = schema.username.validate(data.username);
if (validationResult.error === undefined) {
setIsInNextStage(true);
} }
};
render() { const handleForgetPassword = async () => {
window.location.replace("/login"); try {
await authentication.forgotPassword(form.state.data.username as string);
setEmailSent(true);
const nextData = { ...form.state.data, username: "", password: "" };
form.setState({ data: nextData });
} catch (ex: any) {
form.handleGeneralError(ex);
}
};
const { tfaNeeded, requestTfaRemoval } = this.state.data; const authenticationWorkAround = async () => {
const { isInNextStage, data, emailSent, passwordMaxLength } = this.state; const data = { ...form.state.data, requestTfaRemoval: true };
const result = this.schema.username.validate(data.username); await performLogin(data);
const validEmail = result.error === undefined ? true : false; form.setState({ data });
};
const { tfaNeeded, requestTfaRemoval } = form.state.data as any;
const result = schema.username.validate(form.state.data.username);
const validEmail = result.error === undefined;
if (authentication.getCurrentUser()) return <Navigate to="/" />; if (authentication.getCurrentUser()) return <Navigate to="/" />;
@ -137,43 +128,51 @@ class LoginForm extends Form<any, any, LoginFormState> {
const loginPanel = ( const loginPanel = (
<> <>
<form onSubmit={this.handleSubmit}> <form onSubmit={handleSubmit}>
{this.renderInput( {renderInput(
"username", "username",
"", "",
form.state.data,
form.state.errors,
InputType.text, InputType.text,
isInNextStage, isInNextStage,
undefined, "",
"Email", "Email",
undefined, 0,
undefined, true,
"username", "username",
form.handleChange,
)} )}
{this.renderInput( {renderInput(
"password", "password",
"", "",
form.state.data,
form.state.errors,
InputType.password, InputType.password,
emailSent, emailSent,
undefined, "",
"Password", "Password",
passwordMaxLength, passwordMaxLength,
isInNextStage, isInNextStage,
"current-password", "current-password",
form.handleChange,
)} )}
{!isInNextStage && {!isInNextStage &&
this.renderButton( renderButton(
"Next", "Next",
"login", form.state.errors,
this.handleNextClick,
"next", "next",
handleNextClick,
"login",
validEmail, validEmail,
ButtonType.primary, ButtonType.primary,
true, true,
)} )}
{isInNextStage && ( {isInNextStage && (
<div className="clickables"> <div className="clickables">
{this.renderButton( {renderButton(
"Login", "Login",
form.state.errors,
"login", "login",
undefined, undefined,
"login", "login",
@ -184,10 +183,11 @@ class LoginForm extends Form<any, any, LoginFormState> {
</form> </form>
{isInNextStage && ( {isInNextStage && (
<div className="forgottenLink"> <div className="forgottenLink">
{this.renderButton( {renderButton(
"Forgotten Password", "Forgotten Password",
form.state.errors,
"forgot-password", "forgot-password",
this.handleForgetPassword, handleForgetPassword,
"forgot-password", "forgot-password",
validEmail, validEmail,
ButtonType.secondary, ButtonType.secondary,
@ -200,16 +200,29 @@ class LoginForm extends Form<any, any, LoginFormState> {
If you have a registered account, you will receive an email. If you have a registered account, you will receive an email.
</div> </div>
)} )}
{this.renderError("_general")} {renderError("_general", form.state.errors)}
</> </>
); );
const tfaPanel = ( const tfaPanel = (
<form onSubmit={this.handleSubmit}> <form onSubmit={handleSubmit}>
{this.renderError("_general")} {renderError("_general", form.state.errors)}
{this.renderInput("securityCode", "Authenticate")} {renderInput(
{this.renderButton("Authenticate")} "securityCode",
<Link to="#" onClick={this.authenticationWorkAround}> "Authenticate",
form.state.data,
form.state.errors,
InputType.text,
false,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
{renderButton("Authenticate", form.state.errors, "authenticate")}
<Link to="#" onClick={authenticationWorkAround}>
My Authenticator is not working My Authenticator is not working
</Link> </Link>
</form> </form>
@ -224,7 +237,6 @@ class LoginForm extends Form<any, any, LoginFormState> {
: loginPanel} : loginPanel}
</div> </div>
); );
} };
}
export default LoginForm; export default LoginForm;

View File

@ -5,7 +5,9 @@ import { toast } from "react-toastify";
import { useForm } from "../../../components/common/useForm"; import { useForm } from "../../../components/common/useForm";
import { InputType } from "../../../components/common/Input"; import { InputType } from "../../../components/common/Input";
import Loading from "../../../components/common/Loading"; import Loading from "../../../components/common/Loading";
import TemplateFiller from "../../../components/common/TemplateFiller"; import TemplateFiller, {
TemplateFillerHandle,
} from "../../../components/common/TemplateFiller";
import { GeneralIdRef, MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../../utils/GeneralIdRef";
import { import {
CustomFieldValue, CustomFieldValue,
@ -31,7 +33,7 @@ const SpecificationsDetails: React.FC<SpecificationsDetailsProps> = ({
siteId: string; siteId: string;
specificationId?: string; specificationId?: string;
}>(); }>();
const TemplateFillerRef = useRef<TemplateFiller>(null); const TemplateFillerRef = useRef<TemplateFillerHandle>(null);
const labelName = "Name"; const labelName = "Name";
const labelPrintSpecification = "Print Specification"; const labelPrintSpecification = "Print Specification";
@ -257,7 +259,9 @@ const SpecificationsDetails: React.FC<SpecificationsDetailsProps> = ({
<TemplateFiller <TemplateFiller
templateId={formTemplate} templateId={formTemplate}
formInstanceId={form.state.data.formInstanceId} formInstanceId={
form.state.data.formInstanceId as GeneralIdRef | undefined
}
ref={TemplateFillerRef} ref={TemplateFillerRef}
onValidationChanged={handleValidationChanged} onValidationChanged={handleValidationChanged}
/> />

View File

@ -1,33 +1,38 @@
import React from "react"; import React, { useEffect, useState } from "react";
import Joi from "joi"; import Joi from "joi";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import Form, { FormState, FormData } from "../../components/common/Form"; import { useForm } from "../../components/common/useForm";
import profileService from "./services/profileService"; import profileService from "./services/profileService";
import { InputType } from "../../components/common/Input"; import { InputType } from "../../components/common/Input";
import { TwoFactorAuthenticationSettings } from "./models/TwoFactorAuthenticationSettings"; import { TwoFactorAuthenticationSettings } from "./models/TwoFactorAuthenticationSettings";
import Loading from "../../components/common/Loading"; import Loading from "../../components/common/Loading";
import {
renderError,
renderButton,
renderInput,
renderToggle,
renderDropSection,
} from "../../components/common/formHelpers";
export interface ProfileStateData extends FormData { const Profile: React.FC = () => {
firstName: string; const labelFirstName = "First Name";
middleNames: string; const labelMiddleNames = "Middle Name(s)";
lastName: string; const labelLastName = "Last Name";
email: string; const labelEmail = "E-Mail";
newPassword: string; const labelNewPassword = "New Password";
confirmPassword: string; const labelConfirmPassword = "Confirm Password";
originalUsingTwoFactorAuthentication: boolean; const labelUsingTwoFactorAuthentication = "2 Factor Authentication";
usingTwoFactorAuthentication: boolean; const labelTfaCode = "Authentication code";
tfaCode: string; const labelApply = "Save";
}
export interface ProfileState extends FormState { const [twoFactorAuthenticationSettings, setTwoFactorAuthenticationSettings] =
data: ProfileStateData; useState<TwoFactorAuthenticationSettings>({
twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings; manualEntrySetupCode: "",
} qrCodeImageUrl: "",
});
class Profile extends Form<any, any, ProfileState> { const form = useForm({
state = {
loaded: false, loaded: false,
passwordMaxLenght : 255,
data: { data: {
firstName: "", firstName: "",
middleNames: "", middleNames: "",
@ -37,35 +42,21 @@ class Profile extends Form<any, any, ProfileState> {
confirmPassword: "", confirmPassword: "",
originalUsingTwoFactorAuthentication: false, originalUsingTwoFactorAuthentication: false,
usingTwoFactorAuthentication: false, usingTwoFactorAuthentication: false,
tfaCode: "" tfaCode: "",
}, },
errors: {}, errors: {},
twoFactorAuthenticationSettings: { redirect: "",
manualEntrySetupCode: "", });
qrCodeImageUrl: "",
},
};
labelFirstName = "First Name"; form.schema = {
labelMiddleNames = "Middle Name(s)"; firstName: Joi.string().required().label(labelFirstName),
labelLastName = "Last Name"; middleNames: Joi.string().allow("").required().label(labelMiddleNames),
labelEmail = "E-Mail"; lastName: Joi.string().required().label(labelLastName),
labelNewPassword = "New Password";
labelConfirmPassword = "Confirm Password";
labelUsingTwoFactorAuthentication = "2 Factor Authentication";
labelTfaCode = "Authentication code";
labelApply = "Save";
schema = {
firstName: Joi.string().required().label(this.labelFirstName),
middleNames: Joi.string().allow("").required().label(this.labelMiddleNames),
lastName: Joi.string().required().label(this.labelLastName),
email: Joi.string() email: Joi.string()
.required() .required()
.email({ tlds: { allow: false } }) .email({ tlds: { allow: false } })
.label(this.labelEmail), .label(labelEmail),
newPassword: Joi.string().allow("").min(5).label(this.labelNewPassword), newPassword: Joi.string().allow("").min(5).label(labelNewPassword),
confirmPassword: Joi.string() confirmPassword: Joi.string()
.when("newPassword", { .when("newPassword", {
is: "", is: "",
@ -76,11 +67,11 @@ class Profile extends Form<any, any, ProfileState> {
return e; return e;
}), }),
}) })
.label(this.labelConfirmPassword), .label(labelConfirmPassword),
originalUsingTwoFactorAuthentication: Joi.boolean().required(), originalUsingTwoFactorAuthentication: Joi.boolean().required(),
usingTwoFactorAuthentication: Joi.boolean().required().label(this.labelUsingTwoFactorAuthentication), usingTwoFactorAuthentication: Joi.boolean()
.required()
.label(labelUsingTwoFactorAuthentication),
tfaCode: Joi.string() tfaCode: Joi.string()
.when("originalUsingTwoFactorAuthentication", { .when("originalUsingTwoFactorAuthentication", {
is: Joi.ref("usingTwoFactorAuthentication"), is: Joi.ref("usingTwoFactorAuthentication"),
@ -91,21 +82,30 @@ class Profile extends Form<any, any, ProfileState> {
.length(6) .length(6)
.required() .required()
.error(() => { .error(() => {
const e = new Error("You must enter the code from the authenicator"); const e = new Error(
"You must enter the code from the authenicator",
);
e.name = "tfaCode"; e.name = "tfaCode";
return e; return e;
}), }),
otherwise: Joi.allow("").optional(), otherwise: Joi.allow("").optional(),
}), }),
}) })
.label(this.labelTfaCode), .label(labelTfaCode),
}; };
async componentDidMount() { const loadProfile = async () => {
try { try {
const profile = await profileService.getMyProfile(); const profile = await profileService.getMyProfile();
if (profile) { if (profile) {
const { firstName, middleNames, lastName, email, usingTwoFactorAuthentication, twoFactorAuthenticationSettings } = profile; const {
firstName,
middleNames,
lastName,
email,
usingTwoFactorAuthentication,
twoFactorAuthenticationSettings,
} = profile;
const data = { const data = {
firstName, firstName,
@ -116,39 +116,61 @@ class Profile extends Form<any, any, ProfileState> {
confirmPassword: "", confirmPassword: "",
originalUsingTwoFactorAuthentication: usingTwoFactorAuthentication, originalUsingTwoFactorAuthentication: usingTwoFactorAuthentication,
usingTwoFactorAuthentication, usingTwoFactorAuthentication,
tfaCode: "" tfaCode: "",
}; };
this.setState({ loaded: true, data, twoFactorAuthenticationSettings }); form.setState({ loaded: true, data });
} setTwoFactorAuthenticationSettings(twoFactorAuthenticationSettings);
}
catch(ex: any) {
this.handleGeneralError(ex);
} }
} catch (ex: any) {
form.handleGeneralError(ex);
} }
};
doSubmit = async (buttonName : string) => { useEffect(() => {
void loadProfile();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const doSubmit = async (buttonName: string) => {
try { try {
const { firstName, middleNames, lastName, email, usingTwoFactorAuthentication, tfaCode, newPassword, confirmPassword } = this.state.data; const {
firstName,
middleNames,
lastName,
email,
usingTwoFactorAuthentication,
tfaCode,
newPassword,
confirmPassword,
} = form.state.data;
let password = ""; let password = "";
if (newPassword === confirmPassword) password = newPassword as string;
if (newPassword === confirmPassword) password = newPassword; const response = await profileService.putMyProfile(
firstName as string,
const response = await profileService.putMyProfile(firstName, middleNames, lastName, email, usingTwoFactorAuthentication, tfaCode, password); middleNames as string,
lastName as string,
email as string,
usingTwoFactorAuthentication as boolean,
tfaCode as string,
password,
);
if (response) { if (response) {
await this.componentDidMount(); await loadProfile();
toast.info("Your profile settings have been saved"); toast.info("Your profile settings have been saved");
} }
} } catch (ex: any) {
catch(ex: any) { form.handleGeneralError(ex);
this.handleGeneralError(ex);
} }
}; };
render() { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const { loaded, twoFactorAuthenticationSettings, passwordMaxLenght } = this.state; form.handleSubmit(e, doSubmit);
};
const { usingTwoFactorAuthentication, newPassword } = this.state.data; const { loaded } = form.state;
const { usingTwoFactorAuthentication, newPassword } = form.state.data;
const passwordMaxLength = 255;
const tfaEnabled = usingTwoFactorAuthentication ? "Enabled" : "Disabled"; const tfaEnabled = usingTwoFactorAuthentication ? "Enabled" : "Disabled";
@ -157,48 +179,168 @@ class Profile extends Form<any, any, ProfileState> {
tfaImageBlock = ( tfaImageBlock = (
<React.Fragment> <React.Fragment>
<label>{twoFactorAuthenticationSettings.manualEntrySetupCode}</label> <label>{twoFactorAuthenticationSettings.manualEntrySetupCode}</label>
<img src={twoFactorAuthenticationSettings.qrCodeImageUrl} alt={twoFactorAuthenticationSettings.manualEntrySetupCode} /> <img
src={twoFactorAuthenticationSettings.qrCodeImageUrl}
alt={twoFactorAuthenticationSettings.manualEntrySetupCode}
/>
</React.Fragment> </React.Fragment>
); );
let tfaSection : JSX.Element; const tfaSection = (
tfaSection = (
<div> <div>
{this.renderToggle("usingTwoFactorAuthentication", this.labelUsingTwoFactorAuthentication)} {renderToggle(
"usingTwoFactorAuthentication",
labelUsingTwoFactorAuthentication,
form.state.data,
form.state.errors,
form.handleToggleChange,
)}
{tfaImageBlock} {tfaImageBlock}
{this.renderInput("tfaCode", this.labelTfaCode)} {renderInput(
"tfaCode",
labelTfaCode,
form.state.data,
form.state.errors,
InputType.text,
false,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
</div> </div>
); );
let passwordSection = <React.Fragment>{this.renderInput("newPassword", this.labelNewPassword, InputType.password)}</React.Fragment>; let passwordSection = (
<React.Fragment>
{renderInput(
"newPassword",
labelNewPassword,
form.state.data,
form.state.errors,
InputType.password,
false,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
</React.Fragment>
);
if (newPassword !== "") if (newPassword !== "")
passwordSection = ( passwordSection = (
<React.Fragment> <React.Fragment>
{this.renderInput("newPassword", this.labelNewPassword, InputType.password, false, undefined, "", passwordMaxLenght)} {renderInput(
{this.renderInput("confirmPassword", this.labelConfirmPassword, InputType.password, false, undefined, "", passwordMaxLenght)} "newPassword",
labelNewPassword,
form.state.data,
form.state.errors,
InputType.password,
false,
"",
"",
passwordMaxLength,
true,
undefined,
form.handleChange,
)}
{renderInput(
"confirmPassword",
labelConfirmPassword,
form.state.data,
form.state.errors,
InputType.password,
false,
"",
"",
passwordMaxLength,
true,
undefined,
form.handleChange,
)}
</React.Fragment> </React.Fragment>
); );
return ( return (
<Loading loaded={loaded}> <Loading loaded={loaded}>
<h1>Profile</h1> <h1>Profile</h1>
<form onSubmit={this.handleSubmit}> <form onSubmit={handleSubmit}>
{this.renderError("_general")} {renderError("_general", form.state.errors)}
{this.renderInput("email", this.labelEmail, InputType.text, true)} {renderInput(
{this.renderInput("firstName", this.labelFirstName)} "email",
{this.renderInput("middleNames", this.labelMiddleNames)} labelEmail,
{this.renderInput("lastName", this.labelLastName)} form.state.data,
form.state.errors,
InputType.text,
true,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
{renderInput(
"firstName",
labelFirstName,
form.state.data,
form.state.errors,
InputType.text,
false,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
{renderInput(
"middleNames",
labelMiddleNames,
form.state.data,
form.state.errors,
InputType.text,
false,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
{renderInput(
"lastName",
labelLastName,
form.state.data,
form.state.errors,
InputType.text,
false,
"",
"",
0,
true,
undefined,
form.handleChange,
)}
{passwordSection} {passwordSection}
{this.renderDropSection("turnOnTfa", <label>Two Factor Authentication {tfaEnabled}</label>, tfaSection)} {renderDropSection(
<br/> "turnOnTfa",
{this.renderButton(this.labelApply)} <label>Two Factor Authentication {tfaEnabled}</label>,
tfaSection,
form.state.errors,
)}
<br />
{renderButton(labelApply, form.state.errors, "apply")}
</form> </form>
</Loading> </Loading>
); );
} };
}
export default Profile; export default Profile;