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", "cross-env": "^7.0.3",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"html-react-parser": "^3.0.16", "html-react-parser": "^3.0.16",
"i18next": "^22.5.1",
"i18next-http-backend": "^3.0.2",
"joi": "^17.9.1", "joi": "^17.9.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
@ -40,13 +42,13 @@
"react-bootstrap": "^2.7.4", "react-bootstrap": "^2.7.4",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-i18next": "^12.3.1",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-toastify": "^9.1.2", "react-toastify": "^9.1.2",
"react-toggle": "^4.1.3", "react-toggle": "^4.1.3",
"runtime-env-cra": "^0.2.4", "runtime-env-cra": "^0.2.4",
"sass": "^1.62.0", "sass": "^1.62.0",
"typescript": "^4.7.4",
"web-vitals": "^3.3.1" "web-vitals": "^3.3.1"
}, },
"scripts": { "scripts": {
@ -56,7 +58,11 @@
"start": "concurrently \"npm run start-react\" \"npm run watch-css\" ", "start": "concurrently \"npm run start-react\" \"npm run watch-css\" ",
"build": "react-app-rewired build", "build": "react-app-rewired build",
"test": "react-app-rewired test", "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": { "eslintConfig": {
"extends": [ "extends": [
@ -79,6 +85,9 @@
"devDependencies": { "devDependencies": {
"@types/node": "^20.2.3", "@types/node": "^20.2.3",
"@types/react-toggle": "^4.0.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 { Routes, Route, Navigate, useNavigate } from "react-router-dom";
import { Helmet, HelmetProvider, HtmlProps } from "react-helmet-async"; import { Helmet, HelmetProvider, HtmlProps } from "react-helmet-async";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import { useTranslation } from "react-i18next";
import config from "./config.json"; import config from "./config.json";
import authentication from "./modules/frame/services/authenticationService"; import authentication from "./modules/frame/services/authenticationService";
import ForgotPassword from "./modules/frame/components/ForgotPassword"; import ForgotPassword from "./modules/frame/components/ForgotPassword";
@ -42,129 +43,459 @@ import BlockedIPs from "./modules/blockedIPs/blockedIPs";
import ErrorLogs from "./modules/errorLogs/errorLogs"; import ErrorLogs from "./modules/errorLogs/errorLogs";
import SsoManager from "./modules/manager/ssoManager/ssoManager"; import SsoManager from "./modules/manager/ssoManager/ssoManager";
import SsoProviderDetails from "./modules/manager/ssoManager/SsoProviderDetails"; import SsoProviderDetails from "./modules/manager/ssoManager/SsoProviderDetails";
import { Namespaces } from "./i18n";
function GetSecureRoutes() { function GetSecureRoutes() {
const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? <Route path="/profile" element={<Redirect to="/account/profile"/>}/> const { t } = useTranslation<typeof Namespaces.Common>();
: <Route path="/profile" element={<Mainframe><Profile /></Mainframe>}/>; const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? (
<Route path="/profile" element={<Redirect to="/account/profile" />} />
) : (
<Route
path="/profile"
element={
<Mainframe>
<Profile />
</Mainframe>
}
/>
);
return ( return (
<> <>
<Route path="/audit/:auditId" element={<Mainframe title="Audit logs"><HOCAudit /></Mainframe>}/> <Route
<Route path="/audit" element={<Mainframe title="Audit logs"><HOCAudit /></Mainframe>} /> path="/audit/:auditId"
<Route path="/blockedIPs" element={<Mainframe title="Blocked IP addresses"><BlockedIPs /></Mainframe>} /> element={
<Route path="/exceptionlogs" element={<Mainframe title="Exception Logs"><ErrorLogs /></Mainframe>} /> <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
<Route path="/specifications/:organisationId/:siteId/:specificationId" element={<Mainframe title="Specification Manager"><SpecificationsDetails editMode={true}/></Mainframe>}/> path="/specifications/:organisationId/:siteId/add"
<Route path="/specifications/:organisationId/:siteId" element={<Mainframe title="Specification Manager"><Specifications /></Mainframe>}/> 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
<Route path="/site/:organisationId/:siteId" element={<Mainframe title="Site Manager"><SiteDetails editMode={true} /></Mainframe>}/> path="/site/:organisationId/add"
<Route path="/site/:organisationId" element={<Mainframe title="Site Manager"><Sites/></Mainframe>}/> element={
<Route path="/site/" element={<Navigate replace to="/404" />} /> <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
<Route path="/organisations/add" element={<Mainframe title="e-print"><HOCOrganisationsDetails editMode={false} /></Mainframe>}/> path="/organisations"
<Route path="/organisations/:organisationId" element={<Mainframe title="e-print"><HOCOrganisationsDetails editMode={true}/></Mainframe>}/> 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/" element={<Navigate replace to="/404" />} />
<Route path="/glossaries/add/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossariesDetails editMode={false}/></Mainframe>}/> <Route
<Route path="/glossaries/edit/" element={<Navigate replace to="/404" />}/> path="/glossaries/add/:glossaryId"
<Route path="/glossaries/edit/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossariesDetails editMode={true}/></Mainframe>}/> element={
<Route path="/glossaries" element={<Mainframe title="Glossary Manager"><HOCGlossaries /></Mainframe>}/> <Mainframe title={t("GlossaryManager")}>
<Route path="/glossaries/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossaries /></Mainframe>}/> <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
<Route path="/forms/edit/:formId" element={<Mainframe title="Form Template Manager"><HOCFormsDetails editMode={true} /></Mainframe>}/> path="/forms/add"
<Route path="/forms" element={<Mainframe title="Form Template Manager"><Forms /></Mainframe>}/> 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
<Route path="/customfields/edit/:customFieldId" element={<Mainframe title="Custom Field Manager"><HOCCustomFieldDetails editMode={true}/></Mainframe>}/> path="/customfields/add"
<Route path="/customfields" element={<Mainframe title="Custom Field Manager"><CustomFields /></Mainframe>}/> 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
<Route path="/sequence/edit/:sequenceId" element={<Mainframe title="Sequence Manager"><HOCSequenceDetails editMode={true}/></Mainframe>}/> path="/sequence/add"
<Route path="/sequence" element={<Mainframe title="Sequence Manager"><Sequence /></Mainframe>}/> 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
<Route path="/domains/edit/:domainId/addRole" element={<Mainframe title="Client Domain Manager"><RolesDetails editMode={false} /></Mainframe>}/> path="/domains/add"
<Route path="/domains/edit/:domainId/editRole/:roleId" element={<Mainframe title="Client Domain Manager"><RolesDetails editMode={true} /></Mainframe>}/> element={
<Route path="/domains/edit/:domainId/editRole/:roleId/addUserToRole" element={<Mainframe title="Client Domain Manager"><AddUserToRole editMode={false} /></Mainframe>}/> <Mainframe title={t("ClientDomainManager")}>
<Route path="/domains/edit/:domainId" element={<Mainframe title="Client Domain Manager"><DomainsDetails editMode={true} /></Mainframe>}/> <DomainsDetails editMode={false} />
<Route path="/domains" element={<Mainframe title="Client Domain Manager"><Domains /></Mainframe>}/> </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
<Route path="/users/edit/:userId" element={<Mainframe title="User Manager"><UserDetails editMode={true} /></Mainframe>}/> path="/users/add"
<Route path="/users" element={<Mainframe title="User Manager"><Users /></Mainframe>}/> 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
<Route path="/ssoManager/add" element={<Mainframe title="Sso Manager"><SsoProviderDetails editMode={false}/></Mainframe>}/> path="/ssoManager"
<Route path="/ssoManager/edit/:ssoProviderId" element={<Mainframe title="Sso Manager"><SsoProviderDetails editMode={true} /></Mainframe>}/> element={
<Mainframe title={t("SsoManager")}>
<SsoManager />
{profileRoute} </Mainframe>
<Route path="/logout" element={<Mainframe><Logout /></Mainframe>}/> }
<Route path="/" element={<Mainframe title="e-suite"><HomePage /></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() { function App() {
let navigate = useNavigate(); let navigate = useNavigate();
useEffect(() => { useEffect(() => {
const timer = setInterval(async () => { const timer = setInterval(async () => {
try { try {
if (authentication.hasToken()) { if (authentication.hasToken()) {
await authentication.refreshToken(); await authentication.refreshToken();
if (authentication.tokenExpired()) { if (authentication.tokenExpired()) {
navigate("/login"); navigate("/login");
authentication.logout() authentication.logout();
} }
} }
} } catch (e: any) {
catch (e: any) { console.log(e);
console.log(e); }
} }, 10 * 1000);
}, 10 * 1000); return () => clearInterval(timer);
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" />} />; return (
<HelmetProvider>
var htmlAttributes : HtmlProps = { <Helmet htmlAttributes={htmlAttributes}>
'data-bs-theme' : theme.getPreferredTheme() <title>{config.applicationName}</title>
} </Helmet>
<main>
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { <Routes>
window.location.reload(); <Route path="/env" element={<EnvPage />} />
}) {loginRoute}
<Route
const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? <Route path="/login" element={<Redirect to="/account/login"/>}/> path="/forgot-password"
: <Route path="/login" element={<LoginFrame><LoginForm /></LoginFrame>} />; element={
<LoginFrame>
return ( <ForgotPassword />
<HelmetProvider> </LoginFrame>
<Helmet htmlAttributes={htmlAttributes}> }
<title>{config.applicationName}</title> />
</Helmet> <Route
<main> path="/404"
<Routes> element={
<Route path="/env" element={<EnvPage />} /> <LoginFrame>
{loginRoute} <NotFound />
<Route path="/forgot-password" element={<LoginFrame><ForgotPassword /></LoginFrame>} /> </LoginFrame>
<Route path="/404" element={<LoginFrame><NotFound /></LoginFrame>} /> }
<Route path="/emailuseraction/:token" element={<LoginFrame><EmailUserAction /></LoginFrame>} /> />
{secureRoutes} <Route
<Route path="*" element={<Navigate replace to="/404"/>} /> path="/emailuseraction/:token"
</Routes> element={
<ToastContainer /> <LoginFrame>
</main> <EmailUserAction />
</HelmetProvider> </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"; import Option from "./option";
interface AutocompleteProps { interface AutocompleteProps {
options?: Option[]; options?: Option[];
selectedOptions?: Option[]; selectedOptions?: Option[];
placeholder?: string; placeholder?: string;
onSelect: (item: Option) => void; onSelect: (item: Option) => void;
} }
interface AutocompleteState { interface AutocompleteState {
filteredOptions: Option[]; filteredOptions: Option[];
} }
export default class Autocomplete extends React.PureComponent<AutocompleteProps, AutocompleteState> { export default class Autocomplete extends React.PureComponent<
private inputRef; AutocompleteProps,
constructor(props: AutocompleteProps) { AutocompleteState
super(props); > {
this.state = { filteredOptions: [] } private inputRef;
this.inputRef = React.createRef<HTMLDivElement>(); 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) { private hideAutocomplete = (event: any) => {
if (filterTerm !== "") { if (event.target.classList.contains("autocomplete-text-input")) return;
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() { this.setState({
let filtered = this.props.options?.filter(x => x.name) ?? []; filteredOptions: [],
filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id)); });
this.setState({ };
filteredOptions: filtered
});
}
private hideAutocomplete = (event: any) => { private handleBlur = (event: React.FocusEvent) => {
if (event.target.classList.contains('autocomplete-text-input')) setTimeout(() => {
return; if (
this.inputRef.current &&
this.inputRef.current.contains(document.activeElement)
) {
return;
}
this.setState({ this.setState({ filteredOptions: [] });
filteredOptions: [] }, 0);
}); };
}
private handleBlur = (event: React.FocusEvent) => { componentDidMount() {
setTimeout(() => { document.addEventListener("click", this.hideAutocomplete);
if ( }
this.inputRef.current &&
this.inputRef.current.contains(document.activeElement)
) {
return;
}
this.setState({ filteredOptions: [] }); componentWillUnmount() {
}, 0); document.removeEventListener("click", this.hideAutocomplete);
}; }
componentDidMount() { render() {
document.addEventListener('click', this.hideAutocomplete); const { placeholder, onSelect } = this.props;
} const { filteredOptions } = this.state;
componentWillUnmount() { return (
document.removeEventListener('click', this.hideAutocomplete); <div
} className="autocomplete"
ref={this.inputRef}
render() { onBlur={this.handleBlur}
const { placeholder, onSelect } = this.props; tabIndex={-1}
const { filteredOptions } = this.state; >
<input
return ( className="autocomplete-text-input"
<div className="autocomplete" type="text"
ref={this.inputRef} onChange={(e) => {
onBlur={this.handleBlur} this.filterOptions(e.target.value);
tabIndex={-1} }}
> onFocus={(e) => {
<input this.showOptions();
className="autocomplete-text-input" }}
type="text" placeholder={placeholder}
onChange={(e) => { this.filterOptions(e.target.value) }} />
onFocus={(e) => { this.showOptions() }} {filteredOptions.length > 0 && (
placeholder={placeholder} <ul className="autocomplete-options">
/> {filteredOptions.map((x, i) => (
{filteredOptions.length > 0 && ( <li key={x._id} value={x._id} tabIndex={0}>
<ul className="autocomplete-options"> <button
{filteredOptions.map((x, i) => className="autocomplete-option text-left"
<li onClick={(e) => {
key={x._id} onSelect(x);
value={x._id} this.setState({ filteredOptions: [] });
tabIndex={0} }}
> >
<button {x.name}
className="autocomplete-option text-left" </button>
onClick={(e) => { </li>
onSelect(x); ))}
this.setState({ filteredOptions: [] }) </ul>
}} )}
> </div>
{x.name} );
</button> }
</li>
)}
</ul>
)}
</div>
)
}
} }

View File

@ -1,93 +1,110 @@
import react, { SyntheticEvent } from 'react'; import React, { SyntheticEvent } from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
export enum ButtonType{ export enum ButtonType {
none, none,
primary, primary,
secondary, secondary,
success, success,
danger, danger,
warning, warning,
info, info,
light, light,
dark, dark,
link link,
} }
export interface ButtonProps<T>{ export interface ButtonProps<T> {
testid?: string; testid?: string;
className?: string; className?: string;
id?:string; id?: string;
name?:string; name?: string;
keyValue?: T; keyValue?: T;
children: React.ReactNode; children: React.ReactNode;
buttonType : ButtonType; buttonType: ButtonType;
disabled?: boolean; disabled?: boolean;
to?: string; to?: string;
onClick?: ( keyValue : T | undefined ) => void; onClick?: (keyValue: T | undefined) => void;
} }
class Button<T> extends react.Component<ButtonProps<T>> { function Button<T>({
Click = (e : SyntheticEvent) => { testid,
const {keyValue, onClick} = this.props; className,
id,
if (onClick) name,
{ keyValue,
e.preventDefault(); children,
onClick(keyValue); 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) switch (buttonType) {
{ case ButtonType.primary:
case ButtonType.primary: classNames = "btn btn-primary";
classNames = "btn btn-primary"; break;
break; case ButtonType.secondary:
case ButtonType.secondary: classNames = "btn btn-secondary";
classNames = "btn btn-secondary"; break;
break; case ButtonType.success:
case ButtonType.success: classNames = "btn btn-success";
classNames = "btn btn-success"; break;
break; case ButtonType.danger:
case ButtonType.danger: classNames = "btn btn-danger";
classNames = "btn btn-danger"; break;
break; case ButtonType.warning:
case ButtonType.warning: classNames = "btn btn-warning";
classNames = "btn btn-warning"; break;
break; case ButtonType.info:
case ButtonType.info: classNames = "btn btn-info";
classNames = "btn btn-info"; break;
break; case ButtonType.light:
case ButtonType.light: classNames = "btn btn-light";
classNames = "btn btn-light"; break;
break; case ButtonType.dark:
case ButtonType.dark: classNames = "btn btn-dark";
classNames = "btn btn-dark"; break;
break; case ButtonType.link:
case ButtonType.link: classNames = "btn btn-link";
classNames = "btn btn-link"; break;
break; case ButtonType.none:
case ButtonType.none: classNames = "btn btn-default";
classNames = "btn btn-default" break;
break; }
}
if (className !== undefined) if (className) {
{ classNames += " " + className;
classNames += " " + className; }
}
if (to !== undefined){
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={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 React, { useState, useCallback } from "react";
import Button, { ButtonType } from './Button'; import Button, { ButtonType } from "./Button";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../i18n";
export interface ConfirmButtonProps<T>{ export interface ConfirmButtonProps<T> {
delayMS? : number; delayMS?: number;
buttonType : ButtonType; buttonType: ButtonType;
keyValue: T; keyValue: T;
children: React.ReactNode; children: React.ReactNode;
confirmMessage?: React.ReactNode; confirmMessage?: React.ReactNode;
onClick?: ( keyValue? : T ) => void; onClick?: (keyValue?: T) => void;
} }
export interface ConfirmButtonState{ function ConfirmButton<T>({
firstClick : boolean 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 > { export default ConfirmButton;
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;

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 React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App"; import App from "./App";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); document.getElementById("root") as HTMLElement,
);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>,
); );
// If you want to start measuring performance in your app, pass a function // 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 authentication from "../services/authenticationService";
import '../../../Sass/_leftMenu.scss'; import "../../../Sass/_leftMenu.scss";
import { faCog, faCogs, faHome, faPrint } from "@fortawesome/pro-thin-svg-icons"; import {
faCog,
faCogs,
faHome,
faPrint,
} from "@fortawesome/pro-thin-svg-icons";
import LeftMenuItem from "./LeftMenuItem"; import LeftMenuItem from "./LeftMenuItem";
import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu"; import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n";
interface LeftMenuProps { const LeftMenu: React.FC = () => {
const { t } = useTranslation<typeof Namespaces.Common>();
}
interface LeftMenuState {
openMenuItem? : LOCLeftMenuSubMenu;
}
class LeftMenu extends React.Component<LeftMenuProps, LeftMenuState> {
state : LeftMenuState = {
openMenuItem : undefined
}
componentDidMount(): void { const [openMenuItem, setOpenMenuItem] = useState<LOCLeftMenuSubMenu>();
document.body.addEventListener('click', () => {
this.setState( { openMenuItem : undefined })
}, true);
}
componentDidUpdate(prevProps: Readonly<LeftMenuProps>, prevState: Readonly<LeftMenuState>, snapshot?: any): void { // Close menus when clicking outside
if (prevState === this.state) useEffect(() => {
{ const handleClick = () => setOpenMenuItem(undefined);
this.setState( { openMenuItem : undefined }) document.body.addEventListener("click", handleClick, true);
} return () => document.body.removeEventListener("click", handleClick, true);
} }, []);
handleClick = (menuItem: LOCLeftMenuSubMenu) => { const handleClick = useCallback((menuItem: LOCLeftMenuSubMenu) => {
const { openMenuItem } = this.state; 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 viewAuditLog = authentication.hasAccess("ViewAuditLog");
const viewOrganisation = authentication.hasAccess("ViewOrganisation"); const viewBlockedIPAddresses = authentication.hasAccess(
"ViewBlockedIPAddresses",
const viewUser = authentication.hasAccess("ViewUser" ); );
const viewDomain = authentication.hasAccess("ViewDomain" ); const viewErrorLogs = authentication.hasAccess("ViewErrorLogs");
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 viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs; const viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs;
const { openMenuItem } = this.state;
return ( return (
<> <>
<div className="leftMenu"> <div className="leftMenu">
<LeftMenuItem to="/" icon={faHome} label="Home" /> <LeftMenuItem to="/" icon={faHome} label={t("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>}
</>
);
}
}
{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; export default LeftMenu;

View File

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

View File

@ -1,8 +1,9 @@
export default interface JwtToken { export default interface JwtToken {
expiry: Date; expiry: Date;
primarysid: bigint; primarysid: bigint;
name: string; name: string;
email: string; email: string;
domainid: bigint; domainid: bigint;
securityPrivileges: []; 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": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -20,8 +16,5 @@
"noEmit": true, "noEmit": true,
"jsx": "preserve" "jsx": "preserve"
}, },
"include": [ "include": ["src", "src/types", "i18next-parser.config.js"]
"src",
"src/types"
]
} }