Added multi language support and started refactoring components to be functions instead of classes.

This commit is contained in:
Colin Dawson 2026-01-26 22:21:29 +00:00
parent 6e3ec1c243
commit b559d9260c
15 changed files with 938 additions and 451 deletions

8
i18n-unused.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
srcPaths: ["src/**/*.{ts,tsx}", "!src/components/common/ckeditor/**"],
localesPath: "public/locales",
defaultNamespace: "common",
// Match ANY t("...") call, anywhere in TS/TSX/JSX
translationKeyMatcher: "t\\([\"']([^\"']+)[\"']\\)",
};

15
i18next-parser.config.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
locales: ["en"],
namespaceSeparator: false,
keySeparator: false,
defaultNamespace: "common",
output: "public/locales/$LOCALE/$NAMESPACE.json",
createOldCatalogs: false,
keepRemoved: false,
lexers: {
ts: ["JsxLexer"],
tsx: ["JsxLexer"],
js: ["JsxLexer"],
jsx: ["JsxLexer"],
},
};

View File

@ -31,6 +31,8 @@
"cross-env": "^7.0.3",
"date-fns": "^2.30.0",
"html-react-parser": "^3.0.16",
"i18next": "^22.5.1",
"i18next-http-backend": "^3.0.2",
"joi": "^17.9.1",
"js-cookie": "^3.0.5",
"jwt-decode": "^3.1.2",
@ -40,13 +42,13 @@
"react-bootstrap": "^2.7.4",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
"react-i18next": "^12.3.1",
"react-router-dom": "^6.10.0",
"react-scripts": "^5.0.1",
"react-toastify": "^9.1.2",
"react-toggle": "^4.1.3",
"runtime-env-cra": "^0.2.4",
"sass": "^1.62.0",
"typescript": "^4.7.4",
"web-vitals": "^3.3.1"
},
"scripts": {
@ -56,7 +58,11 @@
"start": "concurrently \"npm run start-react\" \"npm run watch-css\" ",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"i18n:extract": "i18next \"src/**/*.{ts,tsx,js,jsx}\" \"!src/components/common/ckeditor/**\" --config i18next-parser.config.js",
"i18n:unused": "i18n-unused display-unused",
"i18n:missed": "i18n-unused display-missed",
"i18n:check": "npm run i18n:extract && npm run i18n:unused"
},
"eslintConfig": {
"extends": [
@ -79,6 +85,9 @@
"devDependencies": {
"@types/node": "^20.2.3",
"@types/react-toggle": "^4.0.3",
"react-app-rewired": "^2.2.1"
"i18n-unused": "^0.19.0",
"i18next-parser": "^9.3.0",
"react-app-rewired": "^2.2.1",
"typescript": "^4.9.5"
}
}

View File

@ -0,0 +1,28 @@
{
"Admin": "Admin",
"AuditLog": "Audit Logs",
"AuditLogs": "Audit Logs",
"BlockedIPAddresses": "Blocked IP addresses",
"BlockedIPs": "Blocked IPs",
"ClientDomainManager": "Client Domain Manager",
"ClientDomains": "Client Domains",
"CustomFieldManager": "Custom Field Manager",
"CustomFields": "Custom Fields",
"e-print": "e-print",
"e-suite": "e-suite",
"ErrorLogs": "Error Logs",
"ExceptionLogs": "Exception Logs",
"Forms": "Forms",
"FormTemplateManager": "Form Template Manager",
"Glossaries": "Glossaries",
"GlossaryManager": "Glossary Manager",
"Home": "Home",
"Sequence": "Sequence",
"SequenceManager": "Sequence Manager",
"SiteManager": "Site Manager",
"SpecificationManager": "Specification Manager",
"SsoManager": "Sso Manager",
"Support": "Support",
"UserManager": "User Manager",
"Users": "Users"
}

View File

@ -2,6 +2,7 @@ import React, { useEffect } from "react";
import { Routes, Route, Navigate, useNavigate } from "react-router-dom";
import { Helmet, HelmetProvider, HtmlProps } from "react-helmet-async";
import { ToastContainer } from "react-toastify";
import { useTranslation } from "react-i18next";
import config from "./config.json";
import authentication from "./modules/frame/services/authenticationService";
import ForgotPassword from "./modules/frame/components/ForgotPassword";
@ -42,129 +43,459 @@ import BlockedIPs from "./modules/blockedIPs/blockedIPs";
import ErrorLogs from "./modules/errorLogs/errorLogs";
import SsoManager from "./modules/manager/ssoManager/ssoManager";
import SsoProviderDetails from "./modules/manager/ssoManager/SsoProviderDetails";
import { Namespaces } from "./i18n";
function GetSecureRoutes() {
const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? <Route path="/profile" element={<Redirect to="/account/profile"/>}/>
: <Route path="/profile" element={<Mainframe><Profile /></Mainframe>}/>;
const { t } = useTranslation<typeof Namespaces.Common>();
const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? (
<Route path="/profile" element={<Redirect to="/account/profile" />} />
) : (
<Route
path="/profile"
element={
<Mainframe>
<Profile />
</Mainframe>
}
/>
);
return (
<>
<Route path="/audit/:auditId" element={<Mainframe title="Audit logs"><HOCAudit /></Mainframe>}/>
<Route path="/audit" element={<Mainframe title="Audit logs"><HOCAudit /></Mainframe>} />
<Route path="/blockedIPs" element={<Mainframe title="Blocked IP addresses"><BlockedIPs /></Mainframe>} />
<Route path="/exceptionlogs" element={<Mainframe title="Exception Logs"><ErrorLogs /></Mainframe>} />
return (
<>
<Route
path="/audit/:auditId"
element={
<Mainframe title={t("AuditLogs")}>
<HOCAudit />
</Mainframe>
}
/>
<Route
path="/audit"
element={
<Mainframe title={t("AuditLogs")}>
<HOCAudit />
</Mainframe>
}
/>
<Route
path="/blockedIPs"
element={
<Mainframe title={t("BlockedIPAddresses")}>
<BlockedIPs />
</Mainframe>
}
/>
<Route
path="/exceptionlogs"
element={
<Mainframe title={t("ExceptionLogs")}>
<ErrorLogs />
</Mainframe>
}
/>
<Route path="/specifications/:organisationId/:siteId/add" element={<Mainframe title="Specification Manager"><SpecificationsDetails editMode={false}/></Mainframe>}/>
<Route path="/specifications/:organisationId/:siteId/:specificationId" element={<Mainframe title="Specification Manager"><SpecificationsDetails editMode={true}/></Mainframe>}/>
<Route path="/specifications/:organisationId/:siteId" element={<Mainframe title="Specification Manager"><Specifications /></Mainframe>}/>
<Route
path="/specifications/:organisationId/:siteId/add"
element={
<Mainframe title={t("SpecificationManager")}>
<SpecificationsDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/specifications/:organisationId/:siteId/:specificationId"
element={
<Mainframe title={t("SpecificationManager")}>
<SpecificationsDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/specifications/:organisationId/:siteId"
element={
<Mainframe title={t("SpecificationManager")}>
<Specifications />
</Mainframe>
}
/>
<Route path="/site/:organisationId/add" element={<Mainframe title="Site Manager"><SiteDetails editMode={false} /></Mainframe>}/>
<Route path="/site/:organisationId/:siteId" element={<Mainframe title="Site Manager"><SiteDetails editMode={true} /></Mainframe>}/>
<Route path="/site/:organisationId" element={<Mainframe title="Site Manager"><Sites/></Mainframe>}/>
<Route path="/site/" element={<Navigate replace to="/404" />} />
<Route
path="/site/:organisationId/add"
element={
<Mainframe title={t("SiteManager")}>
<SiteDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/site/:organisationId/:siteId"
element={
<Mainframe title={t("SiteManager")}>
<SiteDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/site/:organisationId"
element={
<Mainframe title={t("SiteManager")}>
<Sites />
</Mainframe>
}
/>
<Route path="/site/" element={<Navigate replace to="/404" />} />
<Route path="/organisations" element={<Mainframe title="e-print"><Organisations /></Mainframe>}/>
<Route path="/organisations/add" element={<Mainframe title="e-print"><HOCOrganisationsDetails editMode={false} /></Mainframe>}/>
<Route path="/organisations/:organisationId" element={<Mainframe title="e-print"><HOCOrganisationsDetails editMode={true}/></Mainframe>}/>
<Route
path="/organisations"
element={
<Mainframe title={t("e-print")}>
<Organisations />
</Mainframe>
}
/>
<Route
path="/organisations/add"
element={
<Mainframe title={t("e-print")}>
<HOCOrganisationsDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/organisations/:organisationId"
element={
<Mainframe title={t("e-print")}>
<HOCOrganisationsDetails editMode={true} />
</Mainframe>
}
/>
<Route path="/glossaries/add/" element={<Navigate replace to="/404" />} />
<Route path="/glossaries/add/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossariesDetails editMode={false}/></Mainframe>}/>
<Route path="/glossaries/edit/" element={<Navigate replace to="/404" />}/>
<Route path="/glossaries/edit/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossariesDetails editMode={true}/></Mainframe>}/>
<Route path="/glossaries" element={<Mainframe title="Glossary Manager"><HOCGlossaries /></Mainframe>}/>
<Route path="/glossaries/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossaries /></Mainframe>}/>
<Route path="/glossaries/add/" element={<Navigate replace to="/404" />} />
<Route
path="/glossaries/add/:glossaryId"
element={
<Mainframe title={t("GlossaryManager")}>
<HOCGlossariesDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/glossaries/edit/"
element={<Navigate replace to="/404" />}
/>
<Route
path="/glossaries/edit/:glossaryId"
element={
<Mainframe title={t("GlossaryManager")}>
<HOCGlossariesDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/glossaries"
element={
<Mainframe title={t("GlossaryManager")}>
<HOCGlossaries />
</Mainframe>
}
/>
<Route
path="/glossaries/:glossaryId"
element={
<Mainframe title={t("GlossaryManager")}>
<HOCGlossaries />
</Mainframe>
}
/>
<Route path="/forms/add" element={<Mainframe title="Form Template Manager"><HOCFormsDetails editMode={false}/></Mainframe>}/>
<Route path="/forms/edit/:formId" element={<Mainframe title="Form Template Manager"><HOCFormsDetails editMode={true} /></Mainframe>}/>
<Route path="/forms" element={<Mainframe title="Form Template Manager"><Forms /></Mainframe>}/>
<Route
path="/forms/add"
element={
<Mainframe title={t("FormTemplateManager")}>
<HOCFormsDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/forms/edit/:formId"
element={
<Mainframe title={t("FormTemplateManager")}>
<HOCFormsDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/forms"
element={
<Mainframe title={t("FormTemplateManager")}>
<Forms />
</Mainframe>
}
/>
<Route path="/customfields/add" element={<Mainframe title="Custom Field Manager"><HOCCustomFieldDetails editMode={false}/></Mainframe>}/>
<Route path="/customfields/edit/:customFieldId" element={<Mainframe title="Custom Field Manager"><HOCCustomFieldDetails editMode={true}/></Mainframe>}/>
<Route path="/customfields" element={<Mainframe title="Custom Field Manager"><CustomFields /></Mainframe>}/>
<Route
path="/customfields/add"
element={
<Mainframe title={t("CustomFieldManager")}>
<HOCCustomFieldDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/customfields/edit/:customFieldId"
element={
<Mainframe title={t("CustomFieldManager")}>
<HOCCustomFieldDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/customfields"
element={
<Mainframe title={t("CustomFieldManager")}>
<CustomFields />
</Mainframe>
}
/>
<Route path="/sequence/add" element={<Mainframe title="Sequence Manager"><HOCSequenceDetails editMode={false}/></Mainframe>}/>
<Route path="/sequence/edit/:sequenceId" element={<Mainframe title="Sequence Manager"><HOCSequenceDetails editMode={true}/></Mainframe>}/>
<Route path="/sequence" element={<Mainframe title="Sequence Manager"><Sequence /></Mainframe>}/>
<Route
path="/sequence/add"
element={
<Mainframe title={t("SequenceManager")}>
<HOCSequenceDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/sequence/edit/:sequenceId"
element={
<Mainframe title={t("SequenceManager")}>
<HOCSequenceDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/sequence"
element={
<Mainframe title={t("SequenceManager")}>
<Sequence />
</Mainframe>
}
/>
<Route path="/domains/add" element={<Mainframe title="Client Domain Manager"><DomainsDetails editMode={false}/></Mainframe>}/>
<Route path="/domains/edit/:domainId/addRole" element={<Mainframe title="Client Domain Manager"><RolesDetails editMode={false} /></Mainframe>}/>
<Route path="/domains/edit/:domainId/editRole/:roleId" element={<Mainframe title="Client Domain Manager"><RolesDetails editMode={true} /></Mainframe>}/>
<Route path="/domains/edit/:domainId/editRole/:roleId/addUserToRole" element={<Mainframe title="Client Domain Manager"><AddUserToRole editMode={false} /></Mainframe>}/>
<Route path="/domains/edit/:domainId" element={<Mainframe title="Client Domain Manager"><DomainsDetails editMode={true} /></Mainframe>}/>
<Route path="/domains" element={<Mainframe title="Client Domain Manager"><Domains /></Mainframe>}/>
<Route
path="/domains/add"
element={
<Mainframe title={t("ClientDomainManager")}>
<DomainsDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/domains/edit/:domainId/addRole"
element={
<Mainframe title={t("ClientDomainManager")}>
<RolesDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/domains/edit/:domainId/editRole/:roleId"
element={
<Mainframe title={t("ClientDomainManager")}>
<RolesDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/domains/edit/:domainId/editRole/:roleId/addUserToRole"
element={
<Mainframe title={t("ClientDomainManager")}>
<AddUserToRole editMode={false} />
</Mainframe>
}
/>
<Route
path="/domains/edit/:domainId"
element={
<Mainframe title={t("ClientDomainManager")}>
<DomainsDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/domains"
element={
<Mainframe title={t("ClientDomainManager")}>
<Domains />
</Mainframe>
}
/>
<Route path="/users/add" element={<Mainframe title="User Manager"><UserDetails editMode={false}/></Mainframe>}/>
<Route path="/users/edit/:userId" element={<Mainframe title="User Manager"><UserDetails editMode={true} /></Mainframe>}/>
<Route path="/users" element={<Mainframe title="User Manager"><Users /></Mainframe>}/>
<Route
path="/users/add"
element={
<Mainframe title={t("UserManager")}>
<UserDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/users/edit/:userId"
element={
<Mainframe title={t("UserManager")}>
<UserDetails editMode={true} />
</Mainframe>
}
/>
<Route
path="/users"
element={
<Mainframe title={t("UserManager")}>
<Users />
</Mainframe>
}
/>
<Route path="/ssoManager" element={<Mainframe title="Sso Manager"><SsoManager /></Mainframe>}/>
<Route path="/ssoManager/add" element={<Mainframe title="Sso Manager"><SsoProviderDetails editMode={false}/></Mainframe>}/>
<Route path="/ssoManager/edit/:ssoProviderId" element={<Mainframe title="Sso Manager"><SsoProviderDetails editMode={true} /></Mainframe>}/>
{profileRoute}
<Route path="/logout" element={<Mainframe><Logout /></Mainframe>}/>
<Route path="/" element={<Mainframe title="e-suite"><HomePage /></Mainframe>}/>
</>
);
<Route
path="/ssoManager"
element={
<Mainframe title={t("SsoManager")}>
<SsoManager />
</Mainframe>
}
/>
<Route
path="/ssoManager/add"
element={
<Mainframe title={t("SsoManager")}>
<SsoProviderDetails editMode={false} />
</Mainframe>
}
/>
<Route
path="/ssoManager/edit/:ssoProviderId"
element={
<Mainframe title={t("SsoManager")}>
<SsoProviderDetails editMode={true} />
</Mainframe>
}
/>
{profileRoute}
<Route
path="/logout"
element={
<Mainframe>
<Logout />
</Mainframe>
}
/>
<Route
path="/"
element={
<Mainframe title={t("e-suite")}>
<HomePage />
</Mainframe>
}
/>
</>
);
}
function App() {
let navigate = useNavigate();
let navigate = useNavigate();
useEffect(() => {
const timer = setInterval(async () => {
try {
if (authentication.hasToken()) {
await authentication.refreshToken();
useEffect(() => {
const timer = setInterval(async () => {
try {
if (authentication.hasToken()) {
await authentication.refreshToken();
if (authentication.tokenExpired()) {
navigate("/login");
authentication.logout()
}
}
}
catch (e: any) {
console.log(e);
}
}, 10 * 1000);
return () => clearInterval(timer);
if (authentication.tokenExpired()) {
navigate("/login");
authentication.logout();
}
}
} catch (e: any) {
console.log(e);
}
}, 10 * 1000);
return () => clearInterval(timer);
});
const isSignedIn = authentication.getCurrentUser() != null;
const secureRoutes = isSignedIn ? (
GetSecureRoutes()
) : (
<Route path="/" element={<Navigate to="/login" />} />
);
var htmlAttributes: HtmlProps = {
"data-bs-theme": theme.getPreferredTheme(),
};
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
window.location.reload();
});
const isSignedIn = authentication.getCurrentUser() != null;
const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? (
<Route path="/login" element={<Redirect to="/account/login" />} />
) : (
<Route
path="/login"
element={
<LoginFrame>
<LoginForm />
</LoginFrame>
}
/>
);
const secureRoutes = isSignedIn ? GetSecureRoutes() : <Route path="/" element={<Navigate to="/login" />} />;
var htmlAttributes : HtmlProps = {
'data-bs-theme' : theme.getPreferredTheme()
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
window.location.reload();
})
const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? <Route path="/login" element={<Redirect to="/account/login"/>}/>
: <Route path="/login" element={<LoginFrame><LoginForm /></LoginFrame>} />;
return (
<HelmetProvider>
<Helmet htmlAttributes={htmlAttributes}>
<title>{config.applicationName}</title>
</Helmet>
<main>
<Routes>
<Route path="/env" element={<EnvPage />} />
{loginRoute}
<Route path="/forgot-password" element={<LoginFrame><ForgotPassword /></LoginFrame>} />
<Route path="/404" element={<LoginFrame><NotFound /></LoginFrame>} />
<Route path="/emailuseraction/:token" element={<LoginFrame><EmailUserAction /></LoginFrame>} />
{secureRoutes}
<Route path="*" element={<Navigate replace to="/404"/>} />
</Routes>
<ToastContainer />
</main>
</HelmetProvider>
);
return (
<HelmetProvider>
<Helmet htmlAttributes={htmlAttributes}>
<title>{config.applicationName}</title>
</Helmet>
<main>
<Routes>
<Route path="/env" element={<EnvPage />} />
{loginRoute}
<Route
path="/forgot-password"
element={
<LoginFrame>
<ForgotPassword />
</LoginFrame>
}
/>
<Route
path="/404"
element={
<LoginFrame>
<NotFound />
</LoginFrame>
}
/>
<Route
path="/emailuseraction/:token"
element={
<LoginFrame>
<EmailUserAction />
</LoginFrame>
}
/>
{secureRoutes}
<Route path="*" element={<Navigate replace to="/404" />} />
</Routes>
<ToastContainer />
</main>
</HelmetProvider>
);
}
export default App;
export default App;

View File

@ -2,117 +2,125 @@ import * as React from "react";
import Option from "./option";
interface AutocompleteProps {
options?: Option[];
selectedOptions?: Option[];
placeholder?: string;
onSelect: (item: Option) => void;
options?: Option[];
selectedOptions?: Option[];
placeholder?: string;
onSelect: (item: Option) => void;
}
interface AutocompleteState {
filteredOptions: Option[];
filteredOptions: Option[];
}
export default class Autocomplete extends React.PureComponent<AutocompleteProps, AutocompleteState> {
private inputRef;
constructor(props: AutocompleteProps) {
super(props);
this.state = { filteredOptions: [] }
this.inputRef = React.createRef<HTMLDivElement>();
export default class Autocomplete extends React.PureComponent<
AutocompleteProps,
AutocompleteState
> {
private inputRef;
constructor(props: AutocompleteProps) {
super(props);
this.state = { filteredOptions: [] };
this.inputRef = React.createRef<HTMLDivElement>();
}
private filterOptions(filterTerm: string) {
if (filterTerm !== "") {
let filtered =
this.props.options?.filter((x) =>
x.name.toLowerCase().includes(filterTerm.toLowerCase()),
) ?? [];
filtered = filtered.filter(
(x) => !this.props.selectedOptions?.some((y) => x._id === y._id),
);
this.setState({
filteredOptions: filtered,
});
} else {
this.setState({
filteredOptions: [],
});
}
}
private showOptions() {
let filtered = this.props.options?.filter((x) => x.name) ?? [];
filtered = filtered.filter(
(x) => !this.props.selectedOptions?.some((y) => x._id === y._id),
);
this.setState({
filteredOptions: filtered,
});
}
private filterOptions(filterTerm: string) {
if (filterTerm !== "") {
let filtered = this.props.options?.filter(x => x.name.toLowerCase().includes(filterTerm.toLowerCase())) ?? [];
filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id));
this.setState({
filteredOptions: filtered
});
}
else {
this.setState({
filteredOptions: []
});
}
}
private hideAutocomplete = (event: any) => {
if (event.target.classList.contains("autocomplete-text-input")) return;
private showOptions() {
let filtered = this.props.options?.filter(x => x.name) ?? [];
filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id));
this.setState({
filteredOptions: filtered
});
}
this.setState({
filteredOptions: [],
});
};
private hideAutocomplete = (event: any) => {
if (event.target.classList.contains('autocomplete-text-input'))
return;
private handleBlur = (event: React.FocusEvent) => {
setTimeout(() => {
if (
this.inputRef.current &&
this.inputRef.current.contains(document.activeElement)
) {
return;
}
this.setState({
filteredOptions: []
});
}
this.setState({ filteredOptions: [] });
}, 0);
};
private handleBlur = (event: React.FocusEvent) => {
setTimeout(() => {
if (
this.inputRef.current &&
this.inputRef.current.contains(document.activeElement)
) {
return;
}
componentDidMount() {
document.addEventListener("click", this.hideAutocomplete);
}
this.setState({ filteredOptions: [] });
}, 0);
};
componentWillUnmount() {
document.removeEventListener("click", this.hideAutocomplete);
}
componentDidMount() {
document.addEventListener('click', this.hideAutocomplete);
}
render() {
const { placeholder, onSelect } = this.props;
const { filteredOptions } = this.state;
componentWillUnmount() {
document.removeEventListener('click', this.hideAutocomplete);
}
render() {
const { placeholder, onSelect } = this.props;
const { filteredOptions } = this.state;
return (
<div className="autocomplete"
ref={this.inputRef}
onBlur={this.handleBlur}
tabIndex={-1}
>
<input
className="autocomplete-text-input"
type="text"
onChange={(e) => { this.filterOptions(e.target.value) }}
onFocus={(e) => { this.showOptions() }}
placeholder={placeholder}
/>
{filteredOptions.length > 0 && (
<ul className="autocomplete-options">
{filteredOptions.map((x, i) =>
<li
key={x._id}
value={x._id}
tabIndex={0}
>
<button
className="autocomplete-option text-left"
onClick={(e) => {
onSelect(x);
this.setState({ filteredOptions: [] })
}}
>
{x.name}
</button>
</li>
)}
</ul>
)}
</div>
)
}
return (
<div
className="autocomplete"
ref={this.inputRef}
onBlur={this.handleBlur}
tabIndex={-1}
>
<input
className="autocomplete-text-input"
type="text"
onChange={(e) => {
this.filterOptions(e.target.value);
}}
onFocus={(e) => {
this.showOptions();
}}
placeholder={placeholder}
/>
{filteredOptions.length > 0 && (
<ul className="autocomplete-options">
{filteredOptions.map((x, i) => (
<li key={x._id} value={x._id} tabIndex={0}>
<button
className="autocomplete-option text-left"
onClick={(e) => {
onSelect(x);
this.setState({ filteredOptions: [] });
}}
>
{x.name}
</button>
</li>
))}
</ul>
)}
</div>
);
}
}

View File

@ -1,93 +1,110 @@
import react, { SyntheticEvent } from 'react';
import { Link } from 'react-router-dom';
import React, { SyntheticEvent } from "react";
import { Link } from "react-router-dom";
export enum ButtonType{
none,
primary,
secondary,
success,
danger,
warning,
info,
light,
dark,
link
export enum ButtonType {
none,
primary,
secondary,
success,
danger,
warning,
info,
light,
dark,
link,
}
export interface ButtonProps<T>{
testid?: string;
className?: string;
id?:string;
name?:string;
keyValue?: T;
children: React.ReactNode;
buttonType : ButtonType;
disabled?: boolean;
to?: string;
onClick?: ( keyValue : T | undefined ) => void;
export interface ButtonProps<T> {
testid?: string;
className?: string;
id?: string;
name?: string;
keyValue?: T;
children: React.ReactNode;
buttonType: ButtonType;
disabled?: boolean;
to?: string;
onClick?: (keyValue: T | undefined) => void;
}
class Button<T> extends react.Component<ButtonProps<T>> {
Click = (e : SyntheticEvent) => {
const {keyValue, onClick} = this.props;
if (onClick)
{
e.preventDefault();
onClick(keyValue);
}
function Button<T>({
testid,
className,
id,
name,
keyValue,
children,
buttonType,
disabled,
to,
onClick,
}: ButtonProps<T>) {
const handleClick = (e: SyntheticEvent) => {
if (onClick) {
e.preventDefault();
onClick(keyValue);
}
render() {
const { id, className, children, buttonType, disabled, name, to, testid } = this.props;
};
let classNames = "";
let classNames = "";
switch (buttonType)
{
case ButtonType.primary:
classNames = "btn btn-primary";
break;
case ButtonType.secondary:
classNames = "btn btn-secondary";
break;
case ButtonType.success:
classNames = "btn btn-success";
break;
case ButtonType.danger:
classNames = "btn btn-danger";
break;
case ButtonType.warning:
classNames = "btn btn-warning";
break;
case ButtonType.info:
classNames = "btn btn-info";
break;
case ButtonType.light:
classNames = "btn btn-light";
break;
case ButtonType.dark:
classNames = "btn btn-dark";
break;
case ButtonType.link:
classNames = "btn btn-link";
break;
case ButtonType.none:
classNames = "btn btn-default"
break;
}
switch (buttonType) {
case ButtonType.primary:
classNames = "btn btn-primary";
break;
case ButtonType.secondary:
classNames = "btn btn-secondary";
break;
case ButtonType.success:
classNames = "btn btn-success";
break;
case ButtonType.danger:
classNames = "btn btn-danger";
break;
case ButtonType.warning:
classNames = "btn btn-warning";
break;
case ButtonType.info:
classNames = "btn btn-info";
break;
case ButtonType.light:
classNames = "btn btn-light";
break;
case ButtonType.dark:
classNames = "btn btn-dark";
break;
case ButtonType.link:
classNames = "btn btn-link";
break;
case ButtonType.none:
classNames = "btn btn-default";
break;
}
if (className !== undefined)
{
classNames += " " + className;
}
if (to !== undefined){
return <Link data-testid={testid} id={id} className={classNames} to={to} >{children}</Link>
}
if (className) {
classNames += " " + className;
}
return <button data-testid={testid} id={id} className={classNames} name={name} disabled={disabled} onClick={this.Click}>{children}</button>;
}
if (to) {
return (
<Link data-testid={testid} id={id} className={classNames} to={to}>
{children}
</Link>
);
}
return (
<button
data-testid={testid}
id={id}
className={classNames}
name={name}
disabled={disabled}
onClick={handleClick}
>
{children}
</button>
);
}
export default Button;
export default Button;

View File

@ -1,57 +1,57 @@
import react from 'react';
import Button, { ButtonType } from './Button';
import React, { useState, useCallback } from "react";
import Button, { ButtonType } from "./Button";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../i18n";
export interface ConfirmButtonProps<T>{
delayMS? : number;
buttonType : ButtonType;
keyValue: T;
children: React.ReactNode;
confirmMessage?: React.ReactNode;
onClick?: ( keyValue? : T ) => void;
export interface ConfirmButtonProps<T> {
delayMS?: number;
buttonType: ButtonType;
keyValue: T;
children: React.ReactNode;
confirmMessage?: React.ReactNode;
onClick?: (keyValue?: T) => void;
}
export interface ConfirmButtonState{
firstClick : boolean
function ConfirmButton<T>({
delayMS = 5000,
buttonType,
keyValue,
children,
confirmMessage,
onClick,
}: ConfirmButtonProps<T>) {
const [firstClick, setFirstClick] = useState(false);
const t = useTranslation<typeof Namespaces.Common>();
const handleFirstClick = useCallback(() => {
setFirstClick(true);
setTimeout(() => {
setFirstClick(false);
}, delayMS);
}, [delayMS]);
const handleSecondClick = useCallback(() => {
if (onClick) {
onClick(keyValue);
}
}, [onClick, keyValue]);
return (
<>
{!firstClick && (
<Button buttonType={buttonType} onClick={handleFirstClick}>
{children}
</Button>
)}
{firstClick && (
<Button buttonType={ButtonType.danger} onClick={handleSecondClick}>
{confirmMessage ?? <>t("Are you sure?")</>}
</Button>
)}
</>
);
}
class ConfirmButton<T> extends react.Component<ConfirmButtonProps<T>, ConfirmButtonState > {
state : ConfirmButtonState = {
firstClick : false
}
FirstClick = () => {
const firstClick = true;
this.setState({firstClick});
let { delayMS } = this.props;
if (delayMS === undefined)
delayMS = 5000;
setTimeout(() => {
console.log(`updating state`)
const firstClick = false;
this.setState({firstClick});
}, delayMS);
}
SecondClick = () => {
const {keyValue, onClick} = this.props;
if (onClick)
onClick(keyValue);
}
render() {
const { buttonType, children, confirmMessage } = this.props;
const { firstClick } = this.state;
return (
<>
{!firstClick && <Button buttonType={buttonType} onClick={this.FirstClick}>{children}</Button>}
{firstClick && <Button buttonType={ButtonType.danger} onClick={this.SecondClick}>{confirmMessage!==undefined?confirmMessage:"Are you sure?"}</Button>}
</>
);
}
}
export default ConfirmButton;
export default ConfirmButton;

29
src/i18n.ts Normal file
View File

@ -0,0 +1,29 @@
// /src/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
import { determineInitialLanguage } from "./modules/frame/services/lanugageService";
export const Namespaces = {
Common: "common",
Frame: "frame",
} as const;
export type Namespace = (typeof Namespaces)[keyof typeof Namespaces];
i18n
.use(HttpBackend) // load translations from /public/locales
.use(initReactI18next)
.init({
lng: determineInitialLanguage(),
fallbackLng: "en",
defaultNS: "common",
interpolation: {
escapeValue: false,
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
});
export default i18n;

View File

@ -1,17 +1,19 @@
import "./i18n";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
// If you want to start measuring performance in your app, pass a function

View File

@ -1,92 +1,120 @@
import * as React from "react";
import React, { useEffect, useState, useCallback } from "react";
import authentication from "../services/authenticationService";
import '../../../Sass/_leftMenu.scss';
import { faCog, faCogs, faHome, faPrint } from "@fortawesome/pro-thin-svg-icons";
import "../../../Sass/_leftMenu.scss";
import {
faCog,
faCogs,
faHome,
faPrint,
} from "@fortawesome/pro-thin-svg-icons";
import LeftMenuItem from "./LeftMenuItem";
import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n";
interface LeftMenuProps {
}
interface LeftMenuState {
openMenuItem? : LOCLeftMenuSubMenu;
}
class LeftMenu extends React.Component<LeftMenuProps, LeftMenuState> {
state : LeftMenuState = {
openMenuItem : undefined
}
const LeftMenu: React.FC = () => {
const { t } = useTranslation<typeof Namespaces.Common>();
componentDidMount(): void {
document.body.addEventListener('click', () => {
this.setState( { openMenuItem : undefined })
}, true);
}
const [openMenuItem, setOpenMenuItem] = useState<LOCLeftMenuSubMenu>();
componentDidUpdate(prevProps: Readonly<LeftMenuProps>, prevState: Readonly<LeftMenuState>, snapshot?: any): void {
if (prevState === this.state)
{
this.setState( { openMenuItem : undefined })
}
}
// Close menus when clicking outside
useEffect(() => {
const handleClick = () => setOpenMenuItem(undefined);
document.body.addEventListener("click", handleClick, true);
return () => document.body.removeEventListener("click", handleClick, true);
}, []);
handleClick = (menuItem: LOCLeftMenuSubMenu) => {
const { openMenuItem } = this.state;
const handleClick = useCallback((menuItem: LOCLeftMenuSubMenu) => {
setOpenMenuItem((current) => (current === menuItem ? undefined : menuItem));
}, []);
const newMenuItem = openMenuItem === menuItem ? undefined : menuItem;
// Access checks
const viewOrganisation = authentication.hasAccess("ViewOrganisation");
const viewUser = authentication.hasAccess("ViewUser");
const viewDomain = authentication.hasAccess("ViewDomain");
const viewGlossary = authentication.hasAccess("ViewGlossary");
const viewFormTemplate = authentication.hasAccess("ViewFormTemplate");
const viewField = authentication.hasAccess("ViewField");
const viewSequence = authentication.hasAccess("ViewSequence");
const viewSsoManager = authentication.hasAccess("ViewSsoProviders");
this.setState( { openMenuItem : newMenuItem })
};
const viewAdmin =
viewUser ||
viewDomain ||
viewGlossary ||
viewFormTemplate ||
viewField ||
viewSequence;
render() {
const viewOrganisation = authentication.hasAccess("ViewOrganisation");
const viewUser = authentication.hasAccess("ViewUser" );
const viewDomain = authentication.hasAccess("ViewDomain" );
const viewGlossary = authentication.hasAccess("ViewGlossary");
const viewFormTemplate = authentication.hasAccess("ViewFormTemplate");
const viewField = authentication.hasAccess("ViewField");
const viewSequence = authentication.hasAccess("ViewSequence");
const viewSsoManager = authentication.hasAccess("ViewSsoProviders");
const viewAdmin = viewUser || viewDomain || viewGlossary || viewFormTemplate || viewField || viewSequence;
const viewAuditLog = authentication.hasAccess("ViewAuditLog");
const viewBlockedIPAddresses = authentication.hasAccess("ViewBlockedIPAddresses");
const viewErrorLogs = authentication.hasAccess("ViewErrorLogs");
const viewAuditLog = authentication.hasAccess("ViewAuditLog");
const viewBlockedIPAddresses = authentication.hasAccess(
"ViewBlockedIPAddresses",
);
const viewErrorLogs = authentication.hasAccess("ViewErrorLogs");
const viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs;
const { openMenuItem } = this.state;
const viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs;
return (
<>
<div className="leftMenu">
<LeftMenuItem to="/" icon={faHome} label="Home" />
{viewOrganisation && <LeftMenuItem to="/organisations" icon={faPrint} label="e-print" />}
{viewAdmin && <LeftMenuSubMenu openMenu={openMenuItem} icon={faCog} label="Admin" onClick={this.handleClick} >
{viewUser && <LeftMenuItem to="/users" label="Users"/>}
{viewDomain && <LeftMenuItem to="/domains" label="Client Domains"/>}
{viewGlossary && <LeftMenuItem to="/glossaries" label="Glossaries"/>}
{viewFormTemplate && <LeftMenuItem to="/forms" label="Forms"/>}
{viewField && <LeftMenuItem to="/customfields" label="Custom Fields"/>}
{viewSequence && <LeftMenuItem to="/sequence" label="Sequence"/>}
{viewSsoManager && <LeftMenuItem to="/ssoManager" label="Sso Manager"/>}
</LeftMenuSubMenu>
}
{viewSupport && <LeftMenuSubMenu openMenu={openMenuItem} icon={faCogs} label="Support" onClick={this.handleClick} >
{viewAuditLog && <LeftMenuItem to="/audit" label="Audit Log"/>}
{viewBlockedIPAddresses && <LeftMenuItem to="/blockedips" label="Blocked IPs"/>}
{viewErrorLogs && <LeftMenuItem to="/exceptionlogs" label="Error Logs"/>}
</LeftMenuSubMenu>}
</div>
{openMenuItem && <div className="subbar">{openMenuItem.props.children}</div>}
</>
);
}
}
return (
<>
<div className="leftMenu">
<LeftMenuItem to="/" icon={faHome} label={t("Home")} />
{viewOrganisation && (
<LeftMenuItem to="/organisations" icon={faPrint} label="e-print" />
)}
{viewAdmin && (
<LeftMenuSubMenu
openMenu={openMenuItem}
icon={faCog}
label={t("Admin")}
onClick={handleClick}
>
{viewUser && <LeftMenuItem to="/users" label={t("Users")} />}
{viewDomain && (
<LeftMenuItem to="/domains" label={t("ClientDomains")} />
)}
{viewGlossary && (
<LeftMenuItem to="/glossaries" label={t("Glossaries")} />
)}
{viewFormTemplate && (
<LeftMenuItem to="/forms" label={t("Forms")} />
)}
{viewField && (
<LeftMenuItem to="/customfields" label={t("CustomFields")} />
)}
{viewSequence && (
<LeftMenuItem to="/sequence" label={t("Sequence")} />
)}
{viewSsoManager && (
<LeftMenuItem to="/ssoManager" label={t("SsoManager")} />
)}
</LeftMenuSubMenu>
)}
{viewSupport && (
<LeftMenuSubMenu
openMenu={openMenuItem}
icon={faCogs}
label={t("Support")}
onClick={handleClick}
>
{viewAuditLog && <LeftMenuItem to="/audit" label={t("AuditLog")} />}
{viewBlockedIPAddresses && (
<LeftMenuItem to="/blockedips" label={t("BlockedIPs")} />
)}
{viewErrorLogs && (
<LeftMenuItem to="/exceptionlogs" label={t("ErrorLogs")} />
)}
</LeftMenuSubMenu>
)}
</div>
{openMenuItem && (
<div className="subbar">{openMenuItem.props.children}</div>
)}
</>
);
};
export default LeftMenu;

View File

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

View File

@ -1,8 +1,9 @@
export default interface JwtToken {
expiry: Date;
primarysid: bigint;
name: string;
email: string;
domainid: bigint;
securityPrivileges: [];
expiry: Date;
primarysid: bigint;
name: string;
email: string;
domainid: bigint;
securityPrivileges: [];
language?: string;
}

View File

@ -0,0 +1,21 @@
import { getCurrentUser } from "./authenticationService";
export function determineInitialLanguage() {
// 1. JWT preference
const currentUser = getCurrentUser();
if (currentUser !== null) {
const jwtLang = currentUser.language || null;
if (jwtLang) return jwtLang;
}
// 2. LocalStorage
//const storedLang = localStorage.getItem("appLanguage");
//if (storedLang) return storedLang;
// 3. Browser
const browserLang = navigator.language?.split("-")[0];
if (browserLang) return browserLang;
// 4. Default
return "en";
}

View File

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -20,8 +16,5 @@
"noEmit": true,
"jsx": "preserve"
},
"include": [
"src",
"src/types"
]
"include": ["src", "src/types", "i18next-parser.config.js"]
}