Next round of refactoring done

This commit is contained in:
Colin Dawson 2026-01-30 20:58:58 +00:00
parent 04c1875d5d
commit f3deae0842
18 changed files with 1149 additions and 927 deletions

View File

@ -1,7 +1,10 @@
{ {
"Activate": "Activate", "Activate": "Activate",
"Admin": "Admin", "Admin": "Admin",
"AnEmailWithPasswordResetLinkHasBeenSent": "An email with a password reset link has been sent.",
"AnErrorOccurred": "An error occurred", "AnErrorOccurred": "An error occurred",
"Application": "Application",
"Applications": "Applications",
"AuditLog": "Audit Logs", "AuditLog": "Audit Logs",
"AuditLogs": "Audit Logs", "AuditLogs": "Audit Logs",
"BlockedIPAddresses": "Blocked IP addresses", "BlockedIPAddresses": "Blocked IP addresses",
@ -13,42 +16,49 @@
"ConfirmPassword": "Confirm Password", "ConfirmPassword": "Confirm Password",
"CustomFieldManager": "Custom Field Manager", "CustomFieldManager": "Custom Field Manager",
"CustomFields": "Custom Fields", "CustomFields": "Custom Fields",
"DisableAuthenticator": "Disable Authenticator",
"DisplayName": "Display Name", "DisplayName": "Display Name",
"EntityDisplayName": "Entity Display Name",
"e-print": "e-print", "e-print": "e-print",
"e-suite": "e-suite", "e-suite": "e-suite",
"e-suiteLogo": "e-suite logo",
"EntityDisplayName": "Entity Display Name",
"ErrorLogs": "Error Logs", "ErrorLogs": "Error Logs",
"ExceptionJson": "Exception JSON", "ExceptionJson": "Exception JSON",
"ExceptionLogs": "Exception Logs", "ExceptionLogs": "Exception Logs",
"FailedToDisableAuthenticator": "Failed to disable authenticator:",
"Forms": "Forms", "Forms": "Forms",
"FormTemplateManager": "Form Template Manager", "FormTemplateManager": "Form Template Manager",
"Glossaries": "Glossaries", "Glossaries": "Glossaries",
"GlossaryManager": "Glossary Manager", "GlossaryManager": "Glossary Manager",
"Home": "Home", "Home": "Home",
"Id": "Id", "Id": "Id",
"Application": "Application",
"Message": "Message",
"ShowJSON": "Show JSON",
"ShowStackTrace": "Show stack trace",
"OccuredAt": "Occured At",
"IPAddress": "IP Address", "IPAddress": "IP Address",
"IPAddressUnblocked": "IP Address '{{ip}}' unblocked.", "IPAddressUnblocked": "IP Address '{{ip}}' unblocked.",
"Loading": "Loading", "Loading": "Loading",
"LoggingOut": "Logging out",
"Message": "Message",
"Name": "Name", "Name": "Name",
"NumberOfAttempts": "Number Of Attempts", "NewPassword": "New Password",
"NewValue": "New Value", "NewValue": "New Value",
"NotFound": "Not found",
"NumberOfAttempts": "Number Of Attempts",
"OccuredAt": "Occured At",
"OldValue": "Old Value", "OldValue": "Old Value",
"Password": "Password", "Password": "Password",
"PasswordIsRequired": "Password is required", "PasswordIsRequired": "Password is required",
"PasswordMinLength": "Password must be at least {{minPasswordLength}} characters", "PasswordMinLength": "Password must be at least {{minPasswordLength}} characters",
"PasswordsMustMatch": "You need to confirm by typing exactly the same as the new password", "PasswordsMustMatch": "You need to confirm by typing exactly the same as the new password",
"PressAgainToUnblock": "Press again to unblock", "PressAgainToUnblock": "Press again to unblock",
"ResetPassword": "Reset Password",
"Save": "Save",
"Sequence": "Sequence", "Sequence": "Sequence",
"SequenceManager": "Sequence Manager", "SequenceManager": "Sequence Manager",
"ShowJSON": "Show JSON",
"ShowStackTrace": "Show stack trace",
"SiteManager": "Site Manager", "SiteManager": "Site Manager",
"SpecificationManager": "Specification Manager", "SpecificationManager": "Specification Manager",
"StackTrace": "Stack Trace",
"SsoManager": "Sso Manager", "SsoManager": "Sso Manager",
"StackTrace": "Stack Trace",
"Support": "Support", "Support": "Support",
"SupportingData": "Supporting Data", "SupportingData": "Supporting Data",
"Timing": "Timing", "Timing": "Timing",
@ -56,5 +66,7 @@
"UnblockedInMinutes": "Unblocked In (Minutes)", "UnblockedInMinutes": "Unblocked In (Minutes)",
"UserManager": "User Manager", "UserManager": "User Manager",
"UserName": "User Name", "UserName": "User Name",
"UsernameIsRequired": "Username is required",
"UsernameMustBeValidEmail": "Username must be a valid email",
"Users": "Users" "Users": "Users"
} }

View File

@ -1,15 +1,26 @@
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import logo from "./E-SUITE_logo.svg" import logo from "./E-SUITE_logo.svg";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../i18n/i18n";
interface LogoProps { interface LogoProps {
className? : string; className?: string;
height? : string height?: string;
width? : string width?: string;
alt?: string
} }
const Logo: FunctionComponent<LogoProps> = (props:LogoProps) => { const Logo: FunctionComponent<LogoProps> = (props: LogoProps) => {
return ( <img className={props.className} height={props.height} width={props.width} alt={props.alt} src={logo}/>); const { t } = useTranslation(Namespaces.Common);
}
return (
export default Logo; <img
className={props.className}
height={props.height}
width={props.width}
alt={t("e-suiteLogo") as string}
src={logo}
/>
);
};
export default Logo;

View File

@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n"; import { Namespaces } from "../../../i18n/i18n";
import ExpandableCell from "../../../components/common/ExpandableCell"; import ExpandableCell from "../../../components/common/ExpandableCell";
import { max } from "date-fns";
export default function ErrorLogsTable( export default function ErrorLogsTable(
props: PublishedTableProps<ErrorLog>, props: PublishedTableProps<ErrorLog>,

View File

@ -1,63 +1,63 @@
import Form, { FormState, FormData } from "../../../components/common/Form"; import React, { useState } from "react";
import Joi from "joi";
import authentication from "../services/authenticationService"; import authentication from "../services/authenticationService";
import { IEmailUserAction } from "../models/IEmailUserAction"; import { IEmailUserAction } from "../models/IEmailUserAction";
import { FormData } from "../../../components/common/Form";
import Button, { ButtonType } from "../../../components/common/Button"; import Button, { ButtonType } from "../../../components/common/Button";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
export interface EmailUserActionDiableTwoFactorAuthenticationData extends FormData { export interface EmailUserActionDiableTwoFactorAuthenticationData extends FormData {
authenticatorDisabled: boolean; authenticatorDisabled: boolean;
} }
export interface EmailUserActionDiableTwoFactorAuthenticationState extends FormState { interface Props {
data: EmailUserActionDiableTwoFactorAuthenticationData; emailUserAction: IEmailUserAction;
} }
class EmailUserActionDiableTwoFactorAuthentication extends Form<any, any, EmailUserActionDiableTwoFactorAuthenticationState> { const EmailUserActionDiableTwoFactorAuthentication: React.FC<Props> = ({
state = { emailUserAction,
loaded: true, }) => {
data: { authenticatorDisabled: false }, const { t } = useTranslation<typeof Namespaces.Common>();
errors: {}, const [authenticatorDisabled, setAuthenticatorDisabled] = useState(false);
const LABEL_DISABLE_AUTHENTICATOR = t("DisableAuthenticator");
const handleSubmit = async () => {
const action: IEmailUserAction = {
email: emailUserAction.email,
token: emailUserAction.token,
password: "",
emailActionType: emailUserAction.emailActionType,
}; };
labelChangePassword = "Disable Authenticator"; try {
const callResult = await authentication.completeEmailAction(action);
schema = { if (callResult === 1) {
authenticatorDisabled: Joi.boolean(), setAuthenticatorDisabled(true);
}; }
} catch (error) {
doSubmit = async () => { console.error(t("FailedToDisableAuthenticator"), error);
const { emailUserAction } = this.props;
const action: IEmailUserAction = {
email: emailUserAction.email,
token: emailUserAction.token,
password: "",
emailActionType: emailUserAction.emailActionType,
};
const callResult = await authentication.completeEmailAction(action);
if (callResult === 1) {
let data = { ...this.state.data };
data.authenticatorDisabled = true;
this.setState({ data });
}
};
render() {
const { authenticatorDisabled } = this.state.data;
if (authenticatorDisabled) {
return <div>Your authenticator has been disabled. You can now log in without two factor authentication</div>;
}
return (
<>
<div>Disable two factor authentication</div>
<Button buttonType={ButtonType.link} onClick={this.doSubmit}>Disable Authenticator</Button>
</>
);
} }
} };
if (authenticatorDisabled) {
return (
<div>
Your authenticator has been disabled. You can now log in without two
factor authentication
</div>
);
}
return (
<>
<div>Disable two factor authentication</div>
<Button buttonType={ButtonType.link} onClick={handleSubmit}>
{LABEL_DISABLE_AUTHENTICATOR}
</Button>
</>
);
};
export default EmailUserActionDiableTwoFactorAuthentication; export default EmailUserActionDiableTwoFactorAuthentication;

View File

@ -1,136 +1,173 @@
import Joi from "joi"; import React, { useState } from "react";
import Form, { businessValidationResult, FormData, FormState } from "../../../components/common/Form";
import { InputType } from "../../../components/common/Input"; import { InputType } from "../../../components/common/Input";
import { IEmailUserAction } from "../models/IEmailUserAction"; import { IEmailUserAction } from "../models/IEmailUserAction";
import authentication from "../services/authenticationService"; import authentication from "../services/authenticationService";
import { FormData } from "../../../components/common/Form";
import Input from "../../../components/common/Input";
import Button, { ButtonType } from "../../../components/common/Button";
import ErrorBlock from "../../../components/common/ErrorBlock";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
export interface EmailUserActionPasswordResetData extends FormData { export interface EmailUserActionPasswordResetData extends FormData {
password: string; password: string;
confirmPassword: string; confirmPassword: string;
passwordChanged: boolean; passwordChanged: boolean;
} }
export interface EmailUserActionPasswordResetState extends FormState { interface Props {
data: EmailUserActionPasswordResetData; emailUserAction: IEmailUserAction;
} }
class EmailUserActionPasswordReset extends Form<any, any, EmailUserActionPasswordResetState> { const EmailUserActionPasswordReset: React.FC<Props> = ({ emailUserAction }) => {
state = { const { t } = useTranslation<typeof Namespaces.Common>();
loaded: true, const [password, setPassword] = useState("");
passwordMaxLenght: 255, const [confirmPassword, setConfirmPassword] = useState("");
data: { password: "", confirmPassword: "", passwordChanged: false }, const [passwordChanged, setPasswordChanged] = useState(false);
errors: {}, const [generalError, setGeneralError] = useState("");
hasTwelveCharacters: false, const [errors, setErrors] = useState<{ [key: string]: string }>({});
hasSpecialCharacter: false, const [hasTwelveCharacters, setHasTwelveCharacters] = useState(false);
hasUppercaseLetter: false, const [hasSpecialCharacter, setHasSpecialCharacter] = useState(false);
hasLowercaseLetter: false, const [hasUppercaseLetter, setHasUppercaseLetter] = useState(false);
hasNumber: false const [hasLowercaseLetter, setHasLowercaseLetter] = useState(false);
}; const [hasNumber, setHasNumber] = useState(false);
labelPassword = "New Password"; const LABEL_PASSWORD = t("NewPassword");
labelConfirmPassword = "Confirm Password"; const LABEL_CONFIRM_PASSWORD = t("ConfirmPassword");
labelChangePassword = "Save"; const LABEL_CHANGE_PASSWORD = t("Save");
const PASSWORD_MAX_LENGTH = 255;
schema = { const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
password: Joi.string().required().min(12).label(this.labelPassword), const newPassword = e.currentTarget.value;
confirmPassword: Joi.string() setPassword(newPassword);
.when("password", { setConfirmPassword("");
is: "", setHasNumber(/\d+/g.test(newPassword));
then: Joi.optional(), setHasLowercaseLetter(/[a-z]/g.test(newPassword));
otherwise: Joi.valid(Joi.ref("password")).error(() => { setHasUppercaseLetter(/[A-Z]/g.test(newPassword));
const e = new Error("Passwords must match"); setHasSpecialCharacter(
e.name = "confirmPassword"; /[ ~`! @#$%^&*()_+\-=[\]{};:\\|,.'"<>/?]/.test(newPassword),
return e; );
}), setHasTwelveCharacters(newPassword.length >= 12);
}) };
.label(this.labelConfirmPassword),
passwordChanged: Joi.boolean(), const handleConfirmPasswordChange = (
}; e: React.ChangeEvent<HTMLInputElement>,
) => {
setConfirmPassword(e.currentTarget.value);
};
BusinessValidation(): businessValidationResult | null { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const { password, confirmPassword } = this.state.data; e.preventDefault();
const newErrors: { [key: string]: string } = {};
if (password !== confirmPassword) { const minPasswordLength = 12;
return {
details: [
{
path: "confirmPassword",
message: "You need to confirm by typing exactly the same as the new password",
},
],
};
}
return null; // Validation
if (!password) {
newErrors.password = t("PasswordIsRequired");
} else if (password.length < minPasswordLength) {
newErrors.password = t("PasswordMinLength", {
minPasswordLength: minPasswordLength,
});
} }
handlePasswordChange = async (e: React.ChangeEvent<HTMLInputElement>) => { if (password && password !== confirmPassword) {
const { data } = this.state; newErrors.confirmPassword = t("PasswordsMustMatch");
data.password = e.currentTarget.value;
data.confirmPassword = "";
const stateData = this.state;
stateData.hasNumber = /\d+/g.test(data.password);
stateData.hasLowercaseLetter = /[a-z]/g.test(data.password);
stateData.hasUppercaseLetter = /[A-Z]/g.test(data.password);;
stateData.hasSpecialCharacter = /[ ~`! @#$%^&*()_+\-=[\]{};:\\|,.'"<>/?]/.test(data.password);
stateData.hasTwelveCharacters = data.password.length >= 12;
this.setState(stateData);
};
doSubmit = async (buttonName: string) => {
const { emailUserAction } = this.props;
const { password } = this.state.data;
const action: IEmailUserAction = {
email: emailUserAction.email,
token: emailUserAction.token,
password: password,
emailActionType: emailUserAction.emailActionType,
};
try {
const callResult = await authentication.completeEmailAction(action);
if (callResult === 1) {
let data = { ...this.state.data };
data.passwordChanged = true;
this.setState({ data });
setTimeout(function () {
window.location.replace('/login');
}, 1000);
}
}
catch (ex: any) {
this.handleGeneralError(ex);
}
};
render() {
const { passwordChanged, password, confirmPassword } = this.state.data;
const { hasNumber, hasLowercaseLetter, hasSpecialCharacter, hasUppercaseLetter, hasTwelveCharacters, passwordMaxLenght } = this.state;
const isFormValid = password !== "" && password === confirmPassword && hasNumber && hasLowercaseLetter && hasSpecialCharacter && hasUppercaseLetter && hasTwelveCharacters;
if (passwordChanged) {
return <div className="alert alert-info">Your password has been reset. Please contact your admin if this wasn't you.</div>;
}
return (
<>
<form onSubmit={this.handleSubmit}>
{this.renderError("_general")}
{this.renderInputWithChangeEvent("password", "", InputType.password, undefined, this.handlePasswordChange, undefined, this.labelPassword, passwordMaxLenght)}
<div className={hasTwelveCharacters ? "checked" : "unchecked"}>Password requires a minimum of 12 characters containing a combination of:</div>
<ul>
<li className={hasSpecialCharacter ? "checked" : ""}>At least 1 symbol</li>
<li className={hasNumber ? "checked" : ""}>At least 1 number</li>
<li className={hasLowercaseLetter ? "checked" : ""}>At least 1 lowercase letter</li>
<li className={hasUppercaseLetter ? "checked" : ""}>At least 1 uppercase letter</li>
</ul>
{this.renderInput("confirmPassword", "", InputType.password, undefined, undefined, this.labelConfirmPassword, passwordMaxLenght)}
{this.renderButton(this.labelChangePassword, "save", undefined, undefined, isFormValid)}
</form>
</>
);
} }
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({});
try {
const action: IEmailUserAction = {
email: emailUserAction.email,
token: emailUserAction.token,
password: password,
emailActionType: emailUserAction.emailActionType,
};
const callResult = await authentication.completeEmailAction(action);
if (callResult === 1) {
setPasswordChanged(true);
setTimeout(() => {
window.location.replace("/login");
}, 1000);
}
} catch (ex: any) {
setGeneralError(ex?.message || t("AnErrorOccurred"));
}
};
const isFormValid =
password !== "" &&
password === confirmPassword &&
hasNumber &&
hasLowercaseLetter &&
hasSpecialCharacter &&
hasUppercaseLetter &&
hasTwelveCharacters;
if (passwordChanged) {
return (
<div className="alert alert-info">
Your password has been reset. Please contact your admin if this wasn't
you.
</div>
);
}
return (
<>
<form onSubmit={handleSubmit}>
{generalError && <ErrorBlock error={generalError} />}
<Input
name="password"
label={LABEL_PASSWORD}
type={InputType.password}
value={password}
onChange={handlePasswordChange}
maxLength={PASSWORD_MAX_LENGTH}
error={errors.password}
/>
<div className={hasTwelveCharacters ? "checked" : "unchecked"}>
Password requires a minimum of 12 characters containing a combination
of:
</div>
<ul>
<li className={hasSpecialCharacter ? "checked" : ""}>
At least 1 symbol
</li>
<li className={hasNumber ? "checked" : ""}>At least 1 number</li>
<li className={hasLowercaseLetter ? "checked" : ""}>
At least 1 lowercase letter
</li>
<li className={hasUppercaseLetter ? "checked" : ""}>
At least 1 uppercase letter
</li>
</ul>
<Input
name="confirmPassword"
label={LABEL_CONFIRM_PASSWORD}
type={InputType.password}
value={confirmPassword}
onChange={handleConfirmPasswordChange}
maxLength={PASSWORD_MAX_LENGTH}
error={errors.confirmPassword}
/>
<Button
buttonType={ButtonType.primary}
disabled={!isFormValid}
onClick={() => {}}
>
{LABEL_CHANGE_PASSWORD}
</Button>
</form>
</>
);
};
export default EmailUserActionPasswordReset; export default EmailUserActionPasswordReset;

View File

@ -1,69 +1,86 @@
import Joi from "joi"; import React, { useState } from "react";
import Form, { FormData, FormState } from "../../../components/common/Form"; import { FormData } from "../../../components/common/Form";
import authentication from "../services/authenticationService"; import authentication from "../services/authenticationService";
import Input, { InputType } from "../../../components/common/Input";
import Button, { ButtonType } from "../../../components/common/Button";
import ErrorBlock from "../../../components/common/ErrorBlock";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
export interface ForgotPasswordData extends FormData { export interface ForgotPasswordData extends FormData {
username: string; username: string;
emailSent: boolean; emailSent: boolean;
} }
export interface ForgotPasswordtate extends FormState { const ForgotPassword: React.FC = () => {
data: ForgotPasswordData; const { t } = useTranslation<typeof Namespaces.Common>();
} const [username, setUsername] = useState("");
const [emailSent, setEmailSent] = useState(false);
const [generalError, setGeneralError] = useState("");
const [errors, setErrors] = useState<{ [key: string]: string }>({});
class ForgotPassword extends Form<any, any, ForgotPasswordtate> { const validateEmail = (email: string): boolean => {
state = { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
loaded: true, return emailRegex.test(email);
data: { username: "", emailSent: false }, };
errors: {},
};
schema = { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
username: Joi.string() e.preventDefault();
.required() const newErrors: { [key: string]: string } = {};
.email({ tlds: { allow: false } })
.label("Username"),
emailSent: Joi.boolean().required(),
};
doSubmit = async (buttonName : string) => { // Validation
try { if (!username) {
let { data } = this.state; newErrors.username = t("UsernameIsRequired");
} else if (!validateEmail(username)) {
const response = await authentication.forgotPassword(data.username); newErrors.username = t("UsernameMustBeValidEmail");
if (response) {
data.emailSent = true;
this.setState({ data });
}
}
catch(ex: any) {
this.handleGeneralError(ex);
}
};
render() {
const { emailSent } = this.state.data;
let content = (
<form onSubmit={this.handleSubmit}>
{this.renderError("_general")}
{this.renderInput("username", "Username")}
{this.renderButton("Reset password")}
</form>
);
if (emailSent) {
content = <div>An email with a password reset link has been sent.</div>;
}
return (
<div>
<div className="forgottenLink">
<h1>Forgot password</h1>
</div>
{content}
</div>
);
} }
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({});
try {
const response = await authentication.forgotPassword(username);
if (response) {
setEmailSent(true);
}
} catch (ex: any) {
setGeneralError(ex?.message || t("AnErrorOccurred"));
}
};
let content = (
<form onSubmit={handleSubmit}>
{generalError && <ErrorBlock error={generalError} />}
<Input
type={InputType.text}
name="username"
label={t("Username")}
value={username}
onChange={(e) => setUsername(e.currentTarget.value)}
error={errors.username}
/>
<Button buttonType={ButtonType.primary} onClick={() => {}}>
{t("ResetPassword")}
</Button>
</form>
);
if (emailSent) {
content = <div>{t("AnEmailWithPasswordResetLinkHasBeenSent")}</div>;
}
return (
<div>
<div className="forgottenLink">
<h1>Forgot password</h1>
</div>
{content}
</div>
);
};
export default ForgotPassword; export default ForgotPassword;

View File

@ -1,49 +1,45 @@
import { IconDefinition } from "@fortawesome/pro-thin-svg-icons"; import { IconDefinition } from "@fortawesome/pro-thin-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { Component } from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import withRouter, { RouterProps } from "../../../utils/withRouter";
interface LeftMenuItemProps extends RouterProps { interface LeftMenuItemProps {
to : string; to: string;
icon? : IconDefinition; icon?: IconDefinition;
label : string; label: string;
} }
class LOCLeftMenuItem extends Component<LeftMenuItemProps> { const LeftMenuItem: React.FC<LeftMenuItemProps> = ({ to, icon, label }) => {
isSelected = ():boolean => { const location = useLocation();
const { to } = this.props;
const { pathname } = this.props.router.location;
let isSelected : boolean = false;
if (to === '/' ? (pathname === to) : pathname.toLowerCase().startsWith(to)){
isSelected = true;
}
return isSelected; const isSelected = (): boolean => {
} const pathname = location.pathname;
return to === "/" ? pathname === to : pathname.toLowerCase().startsWith(to);
};
render() { let className = "";
const { to, icon, label } = this.props;
let className = ""; if (isSelected()) {
className += " leftMenuSelected";
}
if (this.isSelected()) { if (icon) {
className += " leftMenuSelected"; return (
} <div className="LeftMenuItem">
<Link className={className} to={to}>
<FontAwesomeIcon className="leftMenuItemIcon" icon={icon} />
<div className="leftMenuItemLabel">{label}</div>
</Link>
</div>
);
}
if ( icon) { return (
return ( <Link className={className} to={to}>
<div className="LeftMenuItem"><Link className={className} to={to} ><FontAwesomeIcon className="leftMenuItemIcon" icon={icon}/><div className="leftMenuItemLabel">{label}</div></Link></div> {label}
); </Link>
} );
};
return (
<Link className={className} to={to} >{label}</Link>
);
}
}
const LeftMenuItem = withRouter(LOCLeftMenuItem);
export default LeftMenuItem; export default LeftMenuItem;
export { LOCLeftMenuItem }; export { LeftMenuItem as LOCLeftMenuItem };

View File

@ -3,71 +3,74 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React from "react"; import React from "react";
import withRouter, { RouterProps } from "../../../utils/withRouter"; import withRouter, { RouterProps } from "../../../utils/withRouter";
interface LeftMenuSubMenuProps extends RouterProps{ interface LeftMenuSubMenuProps extends RouterProps {
icon : IconDefinition; icon: IconDefinition;
label : string; label: string;
openMenu? : LOCLeftMenuSubMenu; openMenu?: LOCLeftMenuSubMenu;
children : (false | JSX.Element)[]; children: (false | JSX.Element)[];
onClick? : ( menuItem : LOCLeftMenuSubMenu ) => void; onClick?: (menuItem: LOCLeftMenuSubMenu) => void;
} }
interface LeftMenuSubMenuState { interface LeftMenuSubMenuState {}
class LOCLeftMenuSubMenu extends React.Component<
LeftMenuSubMenuProps,
LeftMenuSubMenuState
> {
state = {};
handleClick = (): void => {
const { onClick } = this.props;
if (onClick !== undefined) onClick(this);
};
isChildSelected = (child: JSX.Element): boolean => {
const { to } = child.props;
const { pathname } = this.props.router.location;
let isSelected: boolean = false;
if (to === "/" ? pathname === to : pathname.toLowerCase().startsWith(to)) {
isSelected = true;
}
return isSelected;
};
isAnyChildSelected = (): boolean => {
const { children } = this.props;
let childIsSelected = false;
children.forEach((child) => {
if (child === false) {
return;
}
if (this.isChildSelected(child)) childIsSelected = true;
});
return childIsSelected;
};
render() {
const { icon, label, openMenu } = this.props;
const selected = this === openMenu || this.isAnyChildSelected();
let className = "LeftMenuItem leftMenuSubMenu";
if (selected) {
className += " leftMenuSubMenuOpen";
}
return (
<div className={className} onClick={this.handleClick}>
<FontAwesomeIcon className="leftMenuItemIcon" icon={icon} />
<div className="leftMenuItemLabel">{label}</div>
</div>
);
}
} }
class LOCLeftMenuSubMenu extends React.Component<LeftMenuSubMenuProps, LeftMenuSubMenuState> {
state = { }
handleClick = (): void =>
{
const { onClick } = this.props;
if (onClick !== undefined)
onClick(this);
}
isChildSelected = (child : JSX.Element):boolean => {
const { to } = child.props;
const { pathname } = this.props.router.location;
let isSelected : boolean = false;
if (to === '/' ? (pathname === to) : pathname.toLowerCase().startsWith(to)){
isSelected = true;
}
return isSelected;
}
isAnyChildSelected = ():boolean => {
const { children } = this.props;
let childIsSelected = false;
children.forEach(child => {
if (child === false){
return;
}
if (this.isChildSelected(child))
childIsSelected = true;
});
return childIsSelected;
}
render() {
const { icon, label, openMenu } = this.props;
const selected = this === openMenu || this.isAnyChildSelected();
let className = "LeftMenuItem leftMenuSubMenu";
if (selected) {
className += " leftMenuSubMenuOpen";
}
return ( <div className={className} onClick={this.handleClick}><FontAwesomeIcon className="leftMenuItemIcon" icon={icon}/><div className="leftMenuItemLabel">{label}</div></div> );
}
}
const LeftMenuSubMenu = withRouter(LOCLeftMenuSubMenu); const LeftMenuSubMenu = withRouter(LOCLeftMenuSubMenu);
export default LeftMenuSubMenu; export default LeftMenuSubMenu;
export {LOCLeftMenuSubMenu} export { LOCLeftMenuSubMenu };

View File

@ -8,159 +8,223 @@ import { ButtonType } from "../../../components/common/Button";
//import '../../../Sass/login.scss'; //import '../../../Sass/login.scss';
export interface LoginFormStateData extends FormData { export interface LoginFormStateData extends FormData {
username: string; username: string;
password: string; password: string;
tfaNeeded: boolean; tfaNeeded: boolean;
requestTfaRemoval: boolean; requestTfaRemoval: boolean;
securityCode: string; securityCode: string;
} }
export interface LoginFormState extends FormState { export interface LoginFormState extends FormState {
passwordMaxLength: number, passwordMaxLength: number;
isInNextStage: boolean, isInNextStage: boolean;
emailSent: boolean, emailSent: boolean;
data: LoginFormStateData; data: LoginFormStateData;
} }
class LoginForm extends Form<any, any, LoginFormState> { class LoginForm extends Form<any, any, LoginFormState> {
state = { state = {
loaded: true, loaded: true,
passwordMaxLength: 255, passwordMaxLength: 255,
isInNextStage: false, isInNextStage: false,
emailSent: false, emailSent: false,
data: { data: {
username: "", username: "",
password: "", password: "",
tfaNeeded: false, tfaNeeded: false,
requestTfaRemoval: false, requestTfaRemoval: false,
securityCode: "", securityCode: "",
}, },
errors: {}, errors: {},
}; };
schema = { schema = {
username: Joi.string() username: Joi.string()
.required() .required()
.email({ tlds: { allow: false } }) .email({ tlds: { allow: false } })
.label("Email"), .label("Email"),
password: Joi.string().required().label("Password"), password: Joi.string().required().label("Password"),
tfaNeeded: Joi.boolean().required(), tfaNeeded: Joi.boolean().required(),
requestTfaRemoval: Joi.boolean().required(), requestTfaRemoval: Joi.boolean().required(),
securityCode: Joi.string().allow("").label("Authenticate"), securityCode: Joi.string().allow("").label("Authenticate"),
}; };
doSubmit = async (buttonName : string) => { doSubmit = async (buttonName: string) => {
const { data } = this.state; const { data } = this.state;
await this.performLogin(data); await this.performLogin(data);
}; };
handleNextClick = async (event: React.MouseEvent) => { handleNextClick = async (event: React.MouseEvent) => {
const data: LoginFormStateData = { ...this.state.data }; const data: LoginFormStateData = { ...this.state.data };
var validationResult = this.schema.username.validate(data.username); var validationResult = this.schema.username.validate(data.username);
if (validationResult.error === undefined) { if (validationResult.error === undefined) {
const stateData = this.state; const stateData = this.state;
stateData.isInNextStage = true; stateData.isInNextStage = true;
this.setState(stateData); this.setState(stateData);
}
} }
};
handleForgetPassword = async () => { handleForgetPassword = async () => {
try { try {
const stateData = this.state; const stateData = this.state;
await authentication.forgotPassword(stateData.data.username); await authentication.forgotPassword(stateData.data.username);
stateData.emailSent = true; stateData.emailSent = true;
stateData.data.username = ""; stateData.data.username = "";
stateData.data.password = ""; stateData.data.password = "";
this.setState(stateData); this.setState(stateData);
} } catch (ex: any) {
catch (ex: any) { this.handleGeneralError(ex);
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.password, data.securityCode, data.requestTfaRemoval);
switch (result) {
case 1: //requires tfa
const { data } = this.state;
if (data.tfaNeeded === true) {
//TFA removal Request accepted.
} else {
data.tfaNeeded = true;
this.setState({ data });
}
break;
case 2: //logged in
window.location.href = "/";
break;
default:
break; //treat at though not logged in.
}
} catch (ex: any) {
this.handleGeneralError(ex);
}
} }
};
render() { authenticationWorkAround = async () => {
window.location.replace("/login"); const data: LoginFormStateData = { ...this.state.data };
data.requestTfaRemoval = true;
const { tfaNeeded, requestTfaRemoval} = this.state.data;
const { isInNextStage, data, emailSent, passwordMaxLength } = this.state;
const result = this.schema.username.validate(data.username);
const validEmail = (result.error === undefined) ? true : false;
if (authentication.getCurrentUser()) return <Navigate to="/" />; await this.performLogin(data);
const requestTfaRemovalPanel = <div>An email has been sent to you so that you can regain control of your account.</div>; this.setState({ data });
};
const loginPanel = ( private async performLogin(data: LoginFormStateData) {
<><form onSubmit={this.handleSubmit}> try {
{this.renderInput("username", "", InputType.text, isInNextStage, undefined, "Email", undefined, undefined,"username")} let result = await authentication.login(
{this.renderInput("password", "", InputType.password, emailSent, undefined, "Password", passwordMaxLength, isInNextStage, "current-password")} data.username,
{!isInNextStage && this.renderButton("Next", "login", this.handleNextClick, "next", validEmail, ButtonType.primary, true)} data.password,
{isInNextStage && <div className="clickables"> data.securityCode,
{this.renderButton("Login", "login", undefined, "login", !emailSent)} data.requestTfaRemoval,
</div> );
}
</form>
{isInNextStage && <div className="forgottenLink">
{this.renderButton("Forgotten Password", "forgot-password", this.handleForgetPassword, "forgot-password", validEmail, ButtonType.secondary, true)}
</div>}
{emailSent && <div className="alert alert-info emailSent">If you have a registered account, you will receive an email.</div>}
{this.renderError("_general")}
</>
);
const tfaPanel = ( switch (result) {
<form onSubmit={this.handleSubmit}> case 1: //requires tfa
{this.renderError("_general")} const { data } = this.state;
{this.renderInput("securityCode", "Authenticate")}
{this.renderButton("Authenticate")}
<Link to="#" onClick={this.authenticationWorkAround}>
My Authenticator is not working
</Link>
</form>
);
return ( if (data.tfaNeeded === true) {
<div> //TFA removal Request accepted.
{requestTfaRemoval ? requestTfaRemovalPanel : tfaNeeded ? tfaPanel : loginPanel} } else {
data.tfaNeeded = true;
this.setState({ data });
}
break;
case 2: //logged in
window.location.href = "/";
break;
default:
break; //treat at though not logged in.
}
} catch (ex: any) {
this.handleGeneralError(ex);
}
}
render() {
window.location.replace("/login");
const { tfaNeeded, requestTfaRemoval } = this.state.data;
const { isInNextStage, data, emailSent, passwordMaxLength } = this.state;
const result = this.schema.username.validate(data.username);
const validEmail = result.error === undefined ? true : false;
if (authentication.getCurrentUser()) return <Navigate to="/" />;
const requestTfaRemovalPanel = (
<div>
An email has been sent to you so that you can regain control of your
account.
</div>
);
const loginPanel = (
<>
<form onSubmit={this.handleSubmit}>
{this.renderInput(
"username",
"",
InputType.text,
isInNextStage,
undefined,
"Email",
undefined,
undefined,
"username",
)}
{this.renderInput(
"password",
"",
InputType.password,
emailSent,
undefined,
"Password",
passwordMaxLength,
isInNextStage,
"current-password",
)}
{!isInNextStage &&
this.renderButton(
"Next",
"login",
this.handleNextClick,
"next",
validEmail,
ButtonType.primary,
true,
)}
{isInNextStage && (
<div className="clickables">
{this.renderButton(
"Login",
"login",
undefined,
"login",
!emailSent,
)}
</div> </div>
); )}
} </form>
{isInNextStage && (
<div className="forgottenLink">
{this.renderButton(
"Forgotten Password",
"forgot-password",
this.handleForgetPassword,
"forgot-password",
validEmail,
ButtonType.secondary,
true,
)}
</div>
)}
{emailSent && (
<div className="alert alert-info emailSent">
If you have a registered account, you will receive an email.
</div>
)}
{this.renderError("_general")}
</>
);
const tfaPanel = (
<form onSubmit={this.handleSubmit}>
{this.renderError("_general")}
{this.renderInput("securityCode", "Authenticate")}
{this.renderButton("Authenticate")}
<Link to="#" onClick={this.authenticationWorkAround}>
My Authenticator is not working
</Link>
</form>
);
return (
<div>
{requestTfaRemoval
? requestTfaRemovalPanel
: tfaNeeded
? tfaPanel
: loginPanel}
</div>
);
}
} }
export default LoginForm; export default LoginForm;

View File

@ -1,20 +1,20 @@
import * as React from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import authentication from "../services/authenticationService"; import authentication from "../services/authenticationService";
class Logout extends React.Component { const Logout: React.FC = () => {
componentDidMount() { const { t } = useTranslation();
authentication.logout();
if (window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN) {
window.location.href = "/account/logout"
}
else {
window.location.href = "/"
}
}
render() { useEffect(() => {
return <div>Logging out</div>; authentication.logout();
if (window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN) {
window.location.href = "/account/logout";
} else {
window.location.href = "/";
} }
} }, []);
return <div>{t("LoggingOut")}</div>;
};
export default Logout; export default Logout;

View File

@ -1,23 +1,23 @@
import * as React from "react"; import React from "react";
import TopMenu from "./TopMenu"; import TopMenu from "./TopMenu";
import LeftMenu from "./LeftMenu"; import LeftMenu from "./LeftMenu";
import "../../../Sass/_frame.scss"; import "../../../Sass/_frame.scss";
type MainFrameProps = { type MainFrameProps = {
title?: string | undefined | null; title?: string | null;
children?: React.ReactNode; // 👈️ type children children?: React.ReactNode;
}; };
const Mainframe = (props: MainFrameProps): JSX.Element => { const Mainframe: React.FC<MainFrameProps> = ({ title, children }) => {
return ( return (
<div className="frame"> <div className="frame">
<TopMenu title={props.title ? props.title : undefined} /> <TopMenu title={title} />
<div className="frame-row"> <div className="frame-row">
<div className="frame-leftMenu"> <div className="frame-leftMenu">
<LeftMenu /> <LeftMenu />
</div> </div>
<div className="frame-workArea">{props.children}</div> <div className="frame-workArea">{children}</div>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,10 @@
import * as React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
function NotFound() { const NotFound: React.FC = () => {
return <h1>Not found</h1>; const { t } = useTranslation();
}
return <h1>{t("NotFound")}</h1>;
};
export default NotFound; export default NotFound;

View File

@ -1,49 +0,0 @@
import * as React from "react";
export interface SwitchProps {
children: React.ReactNode;
}
export class Switch extends React.PureComponent<SwitchProps> {
render() {
const children = React.Children.toArray(this.props.children);
let caseComponent: any = children.filter((c:any) => {
return c.type === Case && c.props.condition === true;
});
if (!caseComponent || caseComponent.length === 0) {
caseComponent = children.filter((c: any) => c.type === Else);
}
return (
<React.Fragment>
{ caseComponent }
</React.Fragment>
);
}
}
export interface CaseProps {
condition: boolean;
children: React.ReactNode;
}
export class Case extends React.PureComponent<CaseProps> {
render() {
const { condition, children } = this.props;
return (
<React.Fragment>
{ condition ? children : null }
</React.Fragment>
);
}
}
export class Else extends React.PureComponent<SwitchProps>
{
render() {
return this.props.children;
}
}

View File

@ -12,7 +12,7 @@ import { getCurrentUser } from "../services/authenticationService";
import { LanguageSelectorMenuItem } from "./LanguageSelector"; import { LanguageSelectorMenuItem } from "./LanguageSelector";
export interface TopMenuProps { export interface TopMenuProps {
title?: string; title: string | undefined | null;
} }
function TopMenu(props: TopMenuProps) { function TopMenu(props: TopMenuProps) {
@ -21,7 +21,7 @@ function TopMenu(props: TopMenuProps) {
return ( return (
<Navbar className="navbar bg-body-tertiary px-4 Header"> <Navbar className="navbar bg-body-tertiary px-4 Header">
<Navbar.Brand href="/"> <Navbar.Brand href="/">
<Logo alt="esuite logo" /> <Logo />
</Navbar.Brand> </Navbar.Brand>
<div className="navbar-left">{props.title}</div> <div className="navbar-left">{props.title}</div>
<div className="navbar-right"> <div className="navbar-right">

View File

@ -5,34 +5,24 @@ import "../../../Sass/login.scss";
import Logo from "../../../img/logo"; import Logo from "../../../img/logo";
interface LoginFrameProps { interface LoginFrameProps {
children?: JSX.Element children?: JSX.Element;
} }
interface LoginFrameState { const LoginFrame: React.FC<LoginFrameProps> = ({ children }) => {
return (
<div className="container-fluid vh-100">
<div className="col-md-2">
<div className="loginFormContainer">
<div className="col-12 logo">
<Logo height="120px" width="120px" />
</div>
<div className="col-12">{children}</div>
</div>
</div>
} <div className="col-md-8"></div>
</div>
);
};
class LoginFrame extends React.Component<LoginFrameProps, LoginFrameState> { export default LoginFrame;
render() {
const { children } = this.props;
return (<div className="container-fluid vh-100">
<div className="col-md-2">
<div className="loginFormContainer">
<div className="col-12 logo">
<Logo alt="esuite logo" height="120px" width="120px" />
</div>
<div className="col-12">
{children}
</div>
</div>
</div>
<div className="col-md-8"></div>
</div>);
}
}
export default LoginFrame;

View File

@ -1,13 +1,23 @@
import * as React from "react"; import React from "react";
function EnvPage() { const EnvPage: React.FC = () => {
return ( return (
<> <>
<p>This is the Environment</p> <p>This is the Environment</p>
<br></br> <br />
<p>window.__RUNTIME_CONFIG__.API_URL = {window.__RUNTIME_CONFIG__.API_URL}</p> <p>
</> window.__RUNTIME_CONFIG__.NODE_ENV ={" "}
); {window.__RUNTIME_CONFIG__.NODE_ENV}
} </p>
<p>
window.__RUNTIME_CONFIG__.API_URL = {window.__RUNTIME_CONFIG__.API_URL}
</p>
<p>
window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ={" "}
{window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? "true" : "false"}
</p>
</>
);
};
export default EnvPage; export default EnvPage;

View File

@ -1,20 +1,24 @@
import * as React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
function HomePage() { const HomePage: React.FC = () => {
const redirect = ()=> { const { t } = useTranslation();
window.location.href = '/organisations'
}
return ( const redirect = () => {
<div className="fluid-container"> window.location.href = "/organisations";
<h3>Applications</h3> };
<div className="e-printWidget" onClick={redirect}>
<div className="e-print"> return (
<div className="thumbnail alert"></div><div className="label">E-print</div> <div className="fluid-container">
</div> <h3>{t("Applications")}</h3>
</div> <div className="e-printWidget" onClick={redirect}>
<div className="e-print">
<div className="thumbnail alert"></div>
<div className="label">{t("e-print")}</div>
</div> </div>
); </div>
} </div>
);
};
export default HomePage; export default HomePage;

View File

@ -7,351 +7,476 @@ import { InputType } from "../../../components/common/Input";
import { FormState } from "../../../components/common/Form"; import { FormState } from "../../../components/common/Form";
import withRouter from "../../../utils/withRouter"; import withRouter from "../../../utils/withRouter";
import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef";
import customFieldsService, { numberParams, textParams } from "./services/customFieldsService"; import customFieldsService, {
numberParams,
textParams,
} from "./services/customFieldsService";
import Option from "../../../components/common/option"; import Option from "../../../components/common/option";
import { GeneralIdRef } from "./../../../utils/GeneralIdRef"; import { GeneralIdRef } from "./../../../utils/GeneralIdRef";
import { Case, Else, Switch } from "../../frame/components/Switch"; import {
import { CustomFieldValue, SystemGlossaries } from "../glossary/services/glossaryService"; CustomFieldValue,
SystemGlossaries,
} from "../glossary/services/glossaryService";
import Loading from "../../../components/common/Loading"; import Loading from "../../../components/common/Loading";
interface CustomFieldDetailsState extends FormState { interface CustomFieldDetailsState extends FormState {
data: { data: {
name: string; name: string;
fieldType: string; fieldType: string;
multiLine: boolean; multiLine: boolean;
defaultValue: string; defaultValue: string;
minEntries: number; minEntries: number;
maxEntries: string | number | undefined; maxEntries: string | number | undefined;
refElementId: CustomFieldValue[] | GeneralIdRef | undefined; refElementId: CustomFieldValue[] | GeneralIdRef | undefined;
minValue: number | undefined; minValue: number | undefined;
maxValue: number | undefined; maxValue: number | undefined;
step: number | undefined; step: number | undefined;
required: boolean; required: boolean;
}; };
redirect: string; redirect: string;
} }
class CustomFieldDetails extends Form<any, any, CustomFieldDetailsState> { class CustomFieldDetails extends Form<any, any, CustomFieldDetailsState> {
state: CustomFieldDetailsState = { state: CustomFieldDetailsState = {
loaded: false, loaded: false,
data: { data: {
name: "", name: "",
fieldType: "Text", fieldType: "Text",
defaultValue: "", defaultValue: "",
multiLine: false, multiLine: false,
minEntries: 0, minEntries: 0,
maxEntries: 1, maxEntries: 1,
refElementId: undefined, refElementId: undefined,
minValue: undefined, minValue: undefined,
maxValue: undefined, maxValue: undefined,
step: undefined, step: undefined,
required: false, required: false,
}, },
errors: {}, errors: {},
redirect: "", redirect: "",
}; };
labelName = "Name"; labelName = "Name";
labelFieldType = "Field Type"; labelFieldType = "Field Type";
labelMultiLine = "Multi-line"; labelMultiLine = "Multi-line";
labelDefaultValue = "Default Value"; labelDefaultValue = "Default Value";
labelMinValue = "Minimum Value"; labelMinValue = "Minimum Value";
labelMaxValue = "Maximum Value"; labelMaxValue = "Maximum Value";
labelStep = "Step"; labelStep = "Step";
labelRequired = "Required"; labelRequired = "Required";
labelMinEntries = "Min Entries"; labelMinEntries = "Min Entries";
labelMaxEntries = "Max Entries (empty=unlimited)"; labelMaxEntries = "Max Entries (empty=unlimited)";
labelRefElementId = "Sequence/Form/Glossary"; labelRefElementId = "Sequence/Form/Glossary";
labelApply = "Save"; labelApply = "Save";
labelSave = "Save and close"; labelSave = "Save and close";
schema = { schema = {
name: Joi.string().required().max(450).label(this.labelName), name: Joi.string().required().max(450).label(this.labelName),
fieldType: Joi.string().required().label(this.labelFieldType), fieldType: Joi.string().required().label(this.labelFieldType),
multiLine: Joi.boolean().label(this.labelMultiLine), multiLine: Joi.boolean().label(this.labelMultiLine),
minEntries: Joi.number().min(0).label(this.labelMinEntries), minEntries: Joi.number().min(0).label(this.labelMinEntries),
maxEntries: Joi.number().empty("").label(this.labelMaxEntries), maxEntries: Joi.number().empty("").label(this.labelMaxEntries),
refElementId: Joi.when("fieldType", { refElementId: Joi.when("fieldType", {
is: Joi.string().valid("Sequence"), is: Joi.string().valid("Sequence"),
then: Joi.object({ then: Joi.object({
id: Joi.optional(), id: Joi.optional(),
guid: Joi.optional(), guid: Joi.optional(),
}).required(),
}).when("fieldType", {
is: Joi.string().valid("Glossary"),
then: Joi.array()
.min(1)
.items(
Joi.object({
displayValue: Joi.string().optional(),
value: Joi.object({
id: Joi.optional(),
guid: Joi.optional(),
}).required(), }).required(),
}).when("fieldType", { }),
is: Joi.string().valid("Glossary"), )
then: Joi.array() .required(),
.min(1) }),
.items( minValue: Joi.number().allow("").label(this.labelMinValue),
Joi.object({ maxValue: Joi.number().allow("").label(this.labelMaxValue),
displayValue: Joi.string().optional(), step: Joi.number().optional().allow("").min(0).label(this.labelStep),
value: Joi.object({ required: Joi.boolean().label(this.labelRequired),
id: Joi.optional(),
guid: Joi.optional(),
}).required(),
})
)
.required(),
}),
minValue: Joi.number().allow("").label(this.labelMinValue),
maxValue: Joi.number().allow("").label(this.labelMaxValue),
step: Joi.number().optional().allow("").min(0).label(this.labelStep),
required: Joi.boolean().label(this.labelRequired),
//defaultValue: Joi.string().allow("").label(this.labelDefaultValue) //defaultValue: Joi.string().allow("").label(this.labelDefaultValue)
defaultValue: Joi.when("fieldType", { defaultValue: Joi.when("fieldType", {
is: Joi.string().valid("Number"), is: Joi.string().valid("Number"),
then: Joi.when("minValue", { then: Joi.when("minValue", {
is: Joi.any().valid(null, ""), is: Joi.any().valid(null, ""),
then: Joi.number(), then: Joi.number(),
otherwise: Joi.number() otherwise: Joi.number()
.min(Joi.ref("minValue")) .min(Joi.ref("minValue"))
.message('"Default Value" must be greater than or equal to "' + this.labelMinValue + '"'), .message(
}) '"Default Value" must be greater than or equal to "' +
.when("maxValue", { this.labelMinValue +
is: Joi.any().valid(null, ""), '"',
then: Joi.number(), ),
otherwise: Joi.number() })
.max(Joi.ref("maxValue")) .when("maxValue", {
.message('"Default Value" must be less than or equal to "' + this.labelMaxValue + '"'), is: Joi.any().valid(null, ""),
}) then: Joi.number(),
.allow(""), otherwise: Joi.number()
otherwise: Joi.string().allow(""), .max(Joi.ref("maxValue"))
}).label(this.labelDefaultValue), .message(
}; '"Default Value" must be less than or equal to "' +
this.labelMaxValue +
'"',
),
})
.allow(""),
otherwise: Joi.string().allow(""),
}).label(this.labelDefaultValue),
};
doSubmit = async (buttonName: string) => { doSubmit = async (buttonName: string) => {
try { try {
const { name, fieldType } = this.state.data; const { name, fieldType } = this.state.data;
let { refElementId, defaultValue, minEntries, maxEntries, required } = this.state.data; let { refElementId, defaultValue, minEntries, maxEntries, required } =
let numberParams: numberParams | undefined = undefined; this.state.data;
let textParams: textParams | undefined = undefined; let numberParams: numberParams | undefined = undefined;
let params; let textParams: textParams | undefined = undefined;
let refElementIdValue: GeneralIdRef | undefined; let params;
let refElementIdValue: GeneralIdRef | undefined;
switch (fieldType) { switch (fieldType) {
case "Sequence": case "Sequence":
minEntries = 1; minEntries = 1;
maxEntries = 1; maxEntries = 1;
defaultValue = ""; defaultValue = "";
refElementIdValue = refElementId as GeneralIdRef; refElementIdValue = refElementId as GeneralIdRef;
break; break;
case "FormTemplate": case "FormTemplate":
minEntries = 1; minEntries = 1;
maxEntries = 1; maxEntries = 1;
defaultValue = ""; defaultValue = "";
break; break;
case "Domain": case "Domain":
minEntries = required ? 1 : 0; minEntries = required ? 1 : 0;
maxEntries = maxEntries === 0 ? undefined : maxEntries; maxEntries = maxEntries === 0 ? undefined : maxEntries;
defaultValue = ""; defaultValue = "";
break; break;
case "Glossary": case "Glossary":
minEntries = required ? 1 : 0; minEntries = required ? 1 : 0;
maxEntries = maxEntries === 0 ? undefined : maxEntries; maxEntries = maxEntries === 0 ? undefined : maxEntries;
defaultValue = ""; defaultValue = "";
refElementIdValue = (refElementId as CustomFieldValue[])[0].value as GeneralIdRef; refElementIdValue = (refElementId as CustomFieldValue[])[0]
break; .value as GeneralIdRef;
case "Text": break;
minEntries = 1; case "Text":
maxEntries = 1; minEntries = 1;
let { multiLine } = this.state.data; maxEntries = 1;
textParams = { multiLine }; let { multiLine } = this.state.data;
params = textParams; textParams = { multiLine };
refElementIdValue = undefined; params = textParams;
break; refElementIdValue = undefined;
case "Number": break;
refElementIdValue = undefined; case "Number":
let { minValue, maxValue, step } = this.state.data; refElementIdValue = undefined;
numberParams = { minValue, maxValue, step }; let { minValue, maxValue, step } = this.state.data;
params = numberParams; numberParams = { minValue, maxValue, step };
minEntries = required ? 1 : 0; params = numberParams;
maxEntries = 1; minEntries = required ? 1 : 0;
break; maxEntries = 1;
default: break;
refElementIdValue = undefined; default:
} refElementIdValue = undefined;
}
const cleanMaxEntries: Number | undefined = maxEntries === "" ? undefined : Number(maxEntries); const cleanMaxEntries: Number | undefined =
maxEntries === "" ? undefined : Number(maxEntries);
if (this.isEditMode()) { if (this.isEditMode()) {
const { customFieldId } = this.props.router.params;
var generalIdRef = MakeGeneralIdRef(customFieldId);
const response = await customFieldsService.putField(
generalIdRef,
name,
fieldType,
defaultValue,
minEntries,
cleanMaxEntries,
refElementIdValue,
params
);
if (response) {
toast.info("Custom Field edited");
}
} else {
const response = await customFieldsService.postField(name, fieldType, defaultValue, minEntries, cleanMaxEntries, refElementIdValue, params);
if (response) {
toast.info("New Custom Field added");
}
}
if (buttonName === this.labelSave) this.setState({ redirect: "/customfields" });
} catch (ex: any) {
this.handleGeneralError(ex);
}
};
isEditMode = () => {
const { editMode } = this.props;
return editMode;
};
componentDidMount = async () => {
const { customFieldId } = this.props.router.params; const { customFieldId } = this.props.router.params;
if (customFieldId !== undefined) { var generalIdRef = MakeGeneralIdRef(customFieldId);
try { const response = await customFieldsService.putField(
const loadedData = await customFieldsService.getField(customFieldId); generalIdRef,
name,
const { data } = this.state; fieldType,
if (loadedData) { defaultValue,
data.name = loadedData.name; minEntries,
data.fieldType = loadedData.fieldType; cleanMaxEntries,
data.defaultValue = loadedData.defaultValue; refElementIdValue,
data.minEntries = loadedData.minEntries; params,
data.maxEntries = loadedData.maxEntries;
switch (data.fieldType) {
case "Glossary":
let convertedRefElementId: CustomFieldValue = {
value: loadedData.refElementId,
};
data.refElementId = [convertedRefElementId];
data.required = loadedData.minEntries > 0;
break;
case "Sequence":
data.refElementId = loadedData.refElementId;
break;
case "Domain":
data.required = loadedData.minEntries > 0;
break;
}
if (loadedData.parameters !== undefined) {
switch (data.fieldType) {
case "Number":
data.required = loadedData.minEntries > 0;
const parameters: numberParams = JSON.parse(loadedData.parameters);
data.minValue = parameters.minValue ?? undefined;
data.maxValue = parameters.maxValue ?? undefined;
data.step = parameters.step ?? undefined;
break;
case "Text":
const textParameters: textParams = JSON.parse(loadedData.parameters);
data.multiLine = textParameters.multiLine ?? false;
break;
}
}
this.setState({ loaded: true, data });
} else {
this.setState({ loaded: false });
}
} catch (ex: any) {
this.handleGeneralError(ex);
}
}
if (!this.isEditMode()) this.setState({ loaded: true });
};
render() {
const { loaded, redirect } = this.state;
if (redirect !== "") return <Navigate to={redirect} />;
const { fieldType, minValue, maxValue, step } = this.state.data;
let mode = "Add";
if (this.isEditMode()) mode = "Edit";
const fieldTypeOptions: Option[] = [
{ _id: "Text", name: "Text" },
{ _id: "Number", name: "Number" },
// { _id: "Boolean", name: "Boolean" },
// { _id: "Date", name: "Date" },
// { _id: "Time", name: "Time" },
// { _id: "DateTime", name: "DateTime" },
{ _id: "Sequence", name: "Sequence" },
{ _id: "FormTemplate", name: "Form Template" },
{ _id: "Glossary", name: "Glossary" },
{ _id: "Domain", name: "Domain" },
];
switch (fieldType) {
case "Sequence":
this.labelRefElementId = "Sequence";
break;
case "FormTemplate":
this.labelRefElementId = "Form";
break;
case "Glossary":
this.labelRefElementId = "Glossary";
break;
}
return (
<Loading loaded={loaded}>
<h1>{mode} Custom Field</h1>
<form onSubmit={this.handleSubmit}>
{this.renderError("_general")}
{this.renderInput("name", this.labelName, InputType.text)}
{this.renderSelect("fieldType", this.labelFieldType, fieldTypeOptions)}
<Switch>
<Case condition={this.state.data.fieldType === "Domain"}>
{this.renderInput("required", this.labelRequired, InputType.checkbox)}
{this.renderInput("maxEntries", this.labelMaxEntries, InputType.number)}
</Case>
<Case condition={this.state.data.fieldType === "Glossary"}>
{this.renderGlossaryPicker(true, "refElementId", this.labelRefElementId, 1, SystemGlossaries)}
{this.renderInput("required", this.labelRequired, InputType.checkbox)}
{this.renderInput("maxEntries", this.labelMaxEntries, InputType.number)}
</Case>
<Case condition={this.state.data.fieldType === "Sequence"}>
{this.renderSequencePicker(true, "refElementId", this.labelRefElementId)}
</Case>
<Case condition={this.state.data.fieldType === "FormTemplate"}>
<></>
</Case>
<Case condition={this.state.data.fieldType === "Text"}>
{this.renderInput("multiLine", this.labelMultiLine, InputType.checkbox)}
<Case condition={this.state.data.multiLine === true}>{this.renderInputTextarea(true, "defaultValue", this.labelDefaultValue)}</Case>
<Case condition={this.state.data.multiLine === false}>
{this.renderInput("defaultValue", this.labelDefaultValue, InputType.text)}
</Case>
</Case>
<Case condition={this.state.data.fieldType === "Number"}>
{this.renderInput("required", this.labelRequired, InputType.checkbox)}
{this.renderInputNumber("minValue", this.labelMinValue, false, undefined, undefined, maxValue, undefined)}
{this.renderInputNumber("maxValue", this.labelMaxValue, false, undefined, minValue, undefined, undefined)}
{this.renderInput("step", this.labelStep, InputType.number)}
{this.renderInputNumber("defaultValue", this.labelDefaultValue, false, undefined, minValue, maxValue, step)}
</Case>
<Else>
{this.renderInput("defaultValue", this.labelDefaultValue, InputType.text)}
{this.renderInput("minEntries", this.labelMinEntries, InputType.number)}
{this.renderInput("maxEntries", this.labelMaxEntries, InputType.number)}
</Else>
</Switch>
{this.isEditMode() && this.renderButton(this.labelApply)}
{this.renderButton(this.labelSave)}
</form>
</Loading>
); );
if (response) {
toast.info("Custom Field edited");
}
} else {
const response = await customFieldsService.postField(
name,
fieldType,
defaultValue,
minEntries,
cleanMaxEntries,
refElementIdValue,
params,
);
if (response) {
toast.info("New Custom Field added");
}
}
if (buttonName === this.labelSave)
this.setState({ redirect: "/customfields" });
} catch (ex: any) {
this.handleGeneralError(ex);
} }
};
isEditMode = () => {
const { editMode } = this.props;
return editMode;
};
componentDidMount = async () => {
const { customFieldId } = this.props.router.params;
if (customFieldId !== undefined) {
try {
const loadedData = await customFieldsService.getField(customFieldId);
const { data } = this.state;
if (loadedData) {
data.name = loadedData.name;
data.fieldType = loadedData.fieldType;
data.defaultValue = loadedData.defaultValue;
data.minEntries = loadedData.minEntries;
data.maxEntries = loadedData.maxEntries;
switch (data.fieldType) {
case "Glossary":
let convertedRefElementId: CustomFieldValue = {
value: loadedData.refElementId,
};
data.refElementId = [convertedRefElementId];
data.required = loadedData.minEntries > 0;
break;
case "Sequence":
data.refElementId = loadedData.refElementId;
break;
case "Domain":
data.required = loadedData.minEntries > 0;
break;
}
if (loadedData.parameters !== undefined) {
switch (data.fieldType) {
case "Number":
data.required = loadedData.minEntries > 0;
const parameters: numberParams = JSON.parse(
loadedData.parameters,
);
data.minValue = parameters.minValue ?? undefined;
data.maxValue = parameters.maxValue ?? undefined;
data.step = parameters.step ?? undefined;
break;
case "Text":
const textParameters: textParams = JSON.parse(
loadedData.parameters,
);
data.multiLine = textParameters.multiLine ?? false;
break;
}
}
this.setState({ loaded: true, data });
} else {
this.setState({ loaded: false });
}
} catch (ex: any) {
this.handleGeneralError(ex);
}
}
if (!this.isEditMode()) this.setState({ loaded: true });
};
render() {
const { loaded, redirect } = this.state;
if (redirect !== "") return <Navigate to={redirect} />;
const { fieldType, minValue, maxValue, step } = this.state.data;
let mode = "Add";
if (this.isEditMode()) mode = "Edit";
const fieldTypeOptions: Option[] = [
{ _id: "Text", name: "Text" },
{ _id: "Number", name: "Number" },
// { _id: "Boolean", name: "Boolean" },
// { _id: "Date", name: "Date" },
// { _id: "Time", name: "Time" },
// { _id: "DateTime", name: "DateTime" },
{ _id: "Sequence", name: "Sequence" },
{ _id: "FormTemplate", name: "Form Template" },
{ _id: "Glossary", name: "Glossary" },
{ _id: "Domain", name: "Domain" },
];
switch (fieldType) {
case "Sequence":
this.labelRefElementId = "Sequence";
break;
case "FormTemplate":
this.labelRefElementId = "Form";
break;
case "Glossary":
this.labelRefElementId = "Glossary";
break;
}
return (
<Loading loaded={loaded}>
<h1>{mode} Custom Field</h1>
<form onSubmit={this.handleSubmit}>
{this.renderError("_general")}
{this.renderInput("name", this.labelName, InputType.text)}
{this.renderSelect(
"fieldType",
this.labelFieldType,
fieldTypeOptions,
)}
{this.state.data.fieldType === "Domain" && (
<>
{this.renderInput(
"required",
this.labelRequired,
InputType.checkbox,
)}
{this.renderInput(
"maxEntries",
this.labelMaxEntries,
InputType.number,
)}
</>
)}
{this.state.data.fieldType === "Glossary" && (
<>
{this.renderGlossaryPicker(
true,
"refElementId",
this.labelRefElementId,
1,
SystemGlossaries,
)}
{this.renderInput(
"required",
this.labelRequired,
InputType.checkbox,
)}
{this.renderInput(
"maxEntries",
this.labelMaxEntries,
InputType.number,
)}
</>
)}
{this.state.data.fieldType === "Sequence" && (
<>
{this.renderSequencePicker(
true,
"refElementId",
this.labelRefElementId,
)}
</>
)}
{this.state.data.fieldType === "Text" && (
<>
{this.renderInput(
"multiLine",
this.labelMultiLine,
InputType.checkbox,
)}
{this.state.data.multiLine === true &&
this.renderInputTextarea(
true,
"defaultValue",
this.labelDefaultValue,
)}
{this.state.data.multiLine === false &&
this.renderInput(
"defaultValue",
this.labelDefaultValue,
InputType.text,
)}
</>
)}
{this.state.data.fieldType === "Number" && (
<>
{this.renderInput(
"required",
this.labelRequired,
InputType.checkbox,
)}
{this.renderInputNumber(
"minValue",
this.labelMinValue,
false,
undefined,
undefined,
maxValue,
undefined,
)}
{this.renderInputNumber(
"maxValue",
this.labelMaxValue,
false,
undefined,
minValue,
undefined,
undefined,
)}
{this.renderInput("step", this.labelStep, InputType.number)}
{this.renderInputNumber(
"defaultValue",
this.labelDefaultValue,
false,
undefined,
minValue,
maxValue,
step,
)}
</>
)}
{![
"Domain",
"Glossary",
"Sequence",
"FormTemplate",
"Text",
"Number",
].includes(this.state.data.fieldType) && (
<>
{this.renderInput(
"defaultValue",
this.labelDefaultValue,
InputType.text,
)}
{this.renderInput(
"minEntries",
this.labelMinEntries,
InputType.number,
)}
{this.renderInput(
"maxEntries",
this.labelMaxEntries,
InputType.number,
)}
</>
)}
{this.isEditMode() && this.renderButton(this.labelApply)}
{this.renderButton(this.labelSave)}
</form>
</Loading>
);
}
} }
const HOCCustomFieldDetails = withRouter(CustomFieldDetails); const HOCCustomFieldDetails = withRouter(CustomFieldDetails);