Added multi language support and started refactoring components to be functions instead of classes.
This commit is contained in:
parent
b559d9260c
commit
471e239591
@ -55,7 +55,9 @@
|
|||||||
"build-css": "sass src/Sass/global.scss public/styles.css",
|
"build-css": "sass src/Sass/global.scss public/styles.css",
|
||||||
"watch-css": "nodemon -e scss -x \"npm run build-css\" ",
|
"watch-css": "nodemon -e scss -x \"npm run build-css\" ",
|
||||||
"start-react": "cross-env NODE_ENV=development runtime-env-cra --config-name=./public/runtime-env.js && react-app-rewired start",
|
"start-react": "cross-env NODE_ENV=development runtime-env-cra --config-name=./public/runtime-env.js && react-app-rewired start",
|
||||||
|
"prestart": "node scripts/generate-locales.js",
|
||||||
"start": "concurrently \"npm run start-react\" \"npm run watch-css\" ",
|
"start": "concurrently \"npm run start-react\" \"npm run watch-css\" ",
|
||||||
|
"prebuild": "node scripts/generate-locales.js",
|
||||||
"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",
|
||||||
|
|||||||
@ -24,5 +24,7 @@
|
|||||||
"SsoManager": "Sso Manager",
|
"SsoManager": "Sso Manager",
|
||||||
"Support": "Support",
|
"Support": "Support",
|
||||||
"UserManager": "User Manager",
|
"UserManager": "User Manager",
|
||||||
"Users": "Users"
|
"Users": "Users",
|
||||||
|
"Name": "Name",
|
||||||
|
"Loading": "Loading"
|
||||||
}
|
}
|
||||||
|
|||||||
30
public/locales/fr/common.json
Normal file
30
public/locales/fr/common.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"Admin": "Admin (In french)",
|
||||||
|
"AuditLog": "Audit Logs (In french)",
|
||||||
|
"AuditLogs": "Audit Logs (In french)",
|
||||||
|
"BlockedIPAddresses": "Blocked IP addresses (In french)",
|
||||||
|
"BlockedIPs": "Blocked IPs (In french)",
|
||||||
|
"ClientDomainManager": "Client Domain Manager (In french)",
|
||||||
|
"ClientDomains": "Client Domains (In french)",
|
||||||
|
"CustomFieldManager": "Custom Field Manager (In french)",
|
||||||
|
"CustomFields": "Custom Fields (In french)",
|
||||||
|
"e-print": "e-print (In french)",
|
||||||
|
"e-suite": "e-suite (In french)",
|
||||||
|
"ErrorLogs": "Error Logs (In french)",
|
||||||
|
"ExceptionLogs": "Exception Logs (In french)",
|
||||||
|
"Forms": "Forms (In french)",
|
||||||
|
"FormTemplateManager": "Form Template Manager (In french)",
|
||||||
|
"Glossaries": "Glossaries (In french)",
|
||||||
|
"GlossaryManager": "Glossary Manager (In french)",
|
||||||
|
"Home": "Home (In french)",
|
||||||
|
"Sequence": "Sequence (In french)",
|
||||||
|
"SequenceManager": "Sequence Manager (In french)",
|
||||||
|
"SiteManager": "Site Manager (In french)",
|
||||||
|
"SpecificationManager": "Specification Manager (In french)",
|
||||||
|
"SsoManager": "Sso Manager (In french)",
|
||||||
|
"Support": "Support (In french)",
|
||||||
|
"UserManager": "User Manager (In french)",
|
||||||
|
"Users": "Users (In french)",
|
||||||
|
"Name": "Name (In french)",
|
||||||
|
"Loading": "Loading (In french)"
|
||||||
|
}
|
||||||
32
scripts/generate-locales.js
Normal file
32
scripts/generate-locales.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// scripts/generate-locales.js
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const localesDir = path.join(__dirname, "../public/locales");
|
||||||
|
const outputFile = path.join(__dirname, "../src/i18n/generatedLocales.ts");
|
||||||
|
|
||||||
|
// Discover locale folders
|
||||||
|
const rawLocales = fs
|
||||||
|
.readdirSync(localesDir)
|
||||||
|
.filter((name) => fs.statSync(path.join(localesDir, name)).isDirectory());
|
||||||
|
|
||||||
|
// Normalise folder names (e.g., en_GB → en-GB)
|
||||||
|
const locales = rawLocales.map((l) => l.replace("_", "-"));
|
||||||
|
|
||||||
|
// Sort by locale code
|
||||||
|
locales.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
// Only region-specific locales (those with a hyphen)
|
||||||
|
const availableLocales = locales.filter((l) => l.includes("-"));
|
||||||
|
|
||||||
|
// Generate TS file
|
||||||
|
const ts = `
|
||||||
|
// AUTO-GENERATED FILE — DO NOT EDIT MANUALLY
|
||||||
|
|
||||||
|
export const availableLocales = ${JSON.stringify(availableLocales, null, 2)} as const;
|
||||||
|
|
||||||
|
export type Locale = typeof availableLocales[number];
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(outputFile, ts);
|
||||||
|
console.log("Generated generatedLocales.ts");
|
||||||
@ -43,7 +43,7 @@ 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";
|
import { Namespaces } from "./i18n/i18n";
|
||||||
|
|
||||||
function GetSecureRoutes() {
|
function GetSecureRoutes() {
|
||||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useCallback } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import Button, { ButtonType } from "./Button";
|
import Button, { ButtonType } from "./Button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Namespaces } from "../../i18n";
|
import { Namespaces } from "../../i18n/i18n";
|
||||||
|
|
||||||
export interface ConfirmButtonProps<T> {
|
export interface ConfirmButtonProps<T> {
|
||||||
delayMS?: number;
|
delayMS?: number;
|
||||||
@ -21,7 +21,7 @@ function ConfirmButton<T>({
|
|||||||
onClick,
|
onClick,
|
||||||
}: ConfirmButtonProps<T>) {
|
}: ConfirmButtonProps<T>) {
|
||||||
const [firstClick, setFirstClick] = useState(false);
|
const [firstClick, setFirstClick] = useState(false);
|
||||||
const t = useTranslation<typeof Namespaces.Common>();
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
|
|
||||||
const handleFirstClick = useCallback(() => {
|
const handleFirstClick = useCallback(() => {
|
||||||
setFirstClick(true);
|
setFirstClick(true);
|
||||||
@ -47,7 +47,7 @@ function ConfirmButton<T>({
|
|||||||
|
|
||||||
{firstClick && (
|
{firstClick && (
|
||||||
<Button buttonType={ButtonType.danger} onClick={handleSecondClick}>
|
<Button buttonType={ButtonType.danger} onClick={handleSecondClick}>
|
||||||
{confirmMessage ?? <>t("Are you sure?")</>}
|
{confirmMessage ?? t("Are you sure?")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { faAdd } from "@fortawesome/free-solid-svg-icons";
|
import { faAdd } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import React from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { CustomField } from "../../modules/manager/customfields/services/customFieldsService";
|
import { CustomField } from "../../modules/manager/customfields/services/customFieldsService";
|
||||||
import { Paginated } from "../../services/Paginated";
|
import { Paginated } from "../../services/Paginated";
|
||||||
import { GeneralIdRef } from "../../utils/GeneralIdRef";
|
import { GeneralIdRef } from "../../utils/GeneralIdRef";
|
||||||
@ -8,6 +8,8 @@ import CustomFieldPicker from "../pickers/CustomFieldPicker";
|
|||||||
import Column from "./columns";
|
import Column from "./columns";
|
||||||
import Table from "./Table";
|
import Table from "./Table";
|
||||||
import Button, { ButtonType } from "./Button";
|
import Button, { ButtonType } from "./Button";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Namespaces } from "../../i18n/i18n";
|
||||||
|
|
||||||
export type CustomFieldEditorAdd = (newField: CustomField) => void;
|
export type CustomFieldEditorAdd = (newField: CustomField) => void;
|
||||||
export type CustomFieldEditorDelete = (keyValue: any) => void;
|
export type CustomFieldEditorDelete = (keyValue: any) => void;
|
||||||
@ -19,81 +21,91 @@ interface CustomFieldsEditorProps {
|
|||||||
value: CustomField[];
|
value: CustomField[];
|
||||||
exclude: CustomField[];
|
exclude: CustomField[];
|
||||||
onAdd?: CustomFieldEditorAdd;
|
onAdd?: CustomFieldEditorAdd;
|
||||||
onDelete?: CustomFieldEditorDelete
|
onDelete?: CustomFieldEditorDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomFieldsEditorState {
|
const CustomFieldsEditor: React.FC<CustomFieldsEditorProps> = ({
|
||||||
id : GeneralIdRef | undefined,
|
value,
|
||||||
displayName : string | undefined
|
exclude,
|
||||||
}
|
label,
|
||||||
|
onAdd,
|
||||||
|
onDelete,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
|
const [id, setId] = useState<GeneralIdRef | undefined>(undefined);
|
||||||
|
const [displayName, setDisplayName] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
class CustomFieldsEditor extends React.Component<CustomFieldsEditorProps, CustomFieldsEditorState> {
|
const columns: Column<CustomField>[] = useMemo(
|
||||||
columns : Column<CustomField>[] = [
|
() => [{ key: "name", label: t("Name"), order: "asc" }],
|
||||||
{ key: "name", label: "Name", order: "asc" },
|
[],
|
||||||
];
|
);
|
||||||
|
|
||||||
state : CustomFieldsEditorState = {
|
const paginated: Paginated<CustomField> = useMemo(
|
||||||
id : undefined,
|
() => ({
|
||||||
displayName: undefined
|
count: 0,
|
||||||
}
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
totalPages: 0,
|
||||||
|
data: value,
|
||||||
|
}),
|
||||||
|
[value],
|
||||||
|
);
|
||||||
|
|
||||||
handleAdd = () => {
|
const handleAdd = useCallback(() => {
|
||||||
const { onAdd } = this.props;
|
if (!onAdd || !id) return;
|
||||||
|
|
||||||
if (onAdd)
|
|
||||||
{
|
|
||||||
const {id, displayName} = this.state;
|
|
||||||
|
|
||||||
if (id)
|
|
||||||
{
|
|
||||||
const newField: CustomField = {
|
const newField: CustomField = {
|
||||||
id: id.id ?? BigInt(-1),
|
id: id.id ?? BigInt(-1),
|
||||||
name: String(displayName),
|
name: String(displayName),
|
||||||
fieldType: "",
|
fieldType: "",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
minEntries: 1,
|
minEntries: 1,
|
||||||
guid : id.guid
|
guid: id.guid,
|
||||||
};
|
};
|
||||||
|
|
||||||
onAdd(newField);
|
onAdd(newField);
|
||||||
|
|
||||||
this.setState( {
|
setId(undefined);
|
||||||
id : undefined,
|
setDisplayName(undefined);
|
||||||
displayName: undefined
|
}, [onAdd, id, displayName]);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange = (name: string, value: GeneralIdRef, displayValue : string) => {
|
const handleChange = useCallback(
|
||||||
this.setState({
|
(name: string, value: GeneralIdRef, displayValue: string) => {
|
||||||
id : value,
|
setId(value);
|
||||||
displayName : displayValue
|
setDisplayName(displayValue);
|
||||||
});
|
},
|
||||||
};
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { value, exclude, label, onDelete } = this.props;
|
|
||||||
|
|
||||||
const paginated : Paginated<CustomField> = {
|
|
||||||
count: 0,
|
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
totalPages: 0,
|
|
||||||
data: value
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
{label}
|
|
||||||
<Table data={ paginated } keyName="id" columns={this.columns} onDelete={onDelete}/>
|
|
||||||
<div>
|
<div>
|
||||||
<CustomFieldPicker name="customField" label={"Add"} exclude={exclude} value={undefined} onChange={this.handleChange}/>
|
{label}
|
||||||
<Button buttonType={ButtonType.primary} onClick={this.handleAdd} disabled={this.state.id === undefined}>
|
<Table
|
||||||
|
data={paginated}
|
||||||
|
keyName="id"
|
||||||
|
columns={columns}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<CustomFieldPicker
|
||||||
|
name="customField"
|
||||||
|
label="Add"
|
||||||
|
exclude={exclude}
|
||||||
|
value={undefined}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
buttonType={ButtonType.primary}
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={id === undefined}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faAdd} />
|
<FontAwesomeIcon icon={faAdd} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CustomFieldsEditor;
|
export default CustomFieldsEditor;
|
||||||
|
|||||||
@ -1,48 +1,38 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from "react-router-dom";
|
||||||
import TabHeader from './TabHeader';
|
import TabHeader from "./TabHeader";
|
||||||
|
|
||||||
interface HorizontalTabsProps {
|
interface HorizontalTabsProps {
|
||||||
children: JSX.Element[];
|
children: JSX.Element[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HorizontalTabsState{
|
const HorizontalTabs: React.FC<HorizontalTabsProps> = ({ children }) => {
|
||||||
activeTab : string;
|
const [activeTab, setActiveTab] = useState<string>("");
|
||||||
redirect: string;
|
const [redirect, setRedirect] = useState<string>("");
|
||||||
|
|
||||||
|
// Set initial tab on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (children.length > 0) {
|
||||||
|
setActiveTab(children[0].props.label);
|
||||||
|
}
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
const onClickTabItem = useCallback((tab: string) => {
|
||||||
|
setActiveTab((prev) => (prev !== tab ? tab : prev));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const activeTabChildren = useMemo(() => {
|
||||||
|
const match = children.find((child) => child.props.label === activeTab);
|
||||||
|
return match ? match.props.children : <></>;
|
||||||
|
}, [children, activeTab]);
|
||||||
|
|
||||||
|
if (redirect !== "") {
|
||||||
|
return <Navigate to={redirect} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HorizontalTabs extends React.Component<HorizontalTabsProps, HorizontalTabsState> {
|
// If only one tab, just render its content
|
||||||
componentDidMount(): void {
|
if (children.length === 1) {
|
||||||
this.onClickTabItem(this.props.children[0].props.label);
|
return <>{activeTabChildren}</>;
|
||||||
}
|
|
||||||
|
|
||||||
onClickTabItem = (tab : string) => {
|
|
||||||
let { activeTab } = this.state;
|
|
||||||
|
|
||||||
if (activeTab !== tab) {
|
|
||||||
activeTab = tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ activeTab } );
|
|
||||||
};
|
|
||||||
|
|
||||||
state : HorizontalTabsState= {
|
|
||||||
activeTab : "",
|
|
||||||
redirect : ""
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { children } = this.props;
|
|
||||||
const { activeTab, redirect } = this.state;
|
|
||||||
|
|
||||||
if (redirect !== "") return <Navigate to={redirect} />;
|
|
||||||
|
|
||||||
const filteredTabs = children.filter( child => child.props.label === activeTab );
|
|
||||||
|
|
||||||
const activeTabChildren = (filteredTabs?.length > 0) ? filteredTabs[0].props.children : <></>
|
|
||||||
|
|
||||||
if (children?.length === 1) {
|
|
||||||
return (<>{activeTabChildren}</>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,24 +41,21 @@ class HorizontalTabs extends React.Component<HorizontalTabsProps, HorizontalTabs
|
|||||||
<ul className="tab-list">
|
<ul className="tab-list">
|
||||||
{children.map((child) => {
|
{children.map((child) => {
|
||||||
const { label } = child.props;
|
const { label } = child.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabHeader
|
<TabHeader
|
||||||
isActive={label === activeTab}
|
|
||||||
key={label}
|
key={label}
|
||||||
label={label}
|
label={label}
|
||||||
onClick={this.onClickTabItem}
|
isActive={label === activeTab}
|
||||||
|
onClick={onClickTabItem}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{activeTabChildren}
|
<div>{activeTabChildren}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default HorizontalTabs;
|
export default HorizontalTabs;
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import '../../Sass/_forms.scss';
|
import "../../Sass/_forms.scss";
|
||||||
import ErrorBlock from "./ErrorBlock";
|
import ErrorBlock from "./ErrorBlock";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export enum InputType {
|
export enum InputType {
|
||||||
button = "button",
|
button = "button",
|
||||||
checkbox = "checkbox",
|
checkbox = "checkbox",
|
||||||
@ -50,12 +51,28 @@ export interface InputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Input(props: InputProps) {
|
function Input(props: InputProps) {
|
||||||
const { includeLabel, name, label, error, placeHolder, readOnly, type, value, defaultValue, maxLength, hidden, autoComplete, onChange, onTextAreaChange, ...rest } = props;
|
const {
|
||||||
|
includeLabel,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
placeHolder,
|
||||||
|
readOnly,
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
maxLength,
|
||||||
|
hidden,
|
||||||
|
autoComplete,
|
||||||
|
onChange,
|
||||||
|
onTextAreaChange,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
let showValue = value;
|
let showValue = value;
|
||||||
let checked: boolean = false;
|
let checked: boolean = false;
|
||||||
|
|
||||||
let divClassName = "form-group"
|
let divClassName = "form-group";
|
||||||
let labelClassName = "label";
|
let labelClassName = "label";
|
||||||
let className = "form-control";
|
let className = "form-control";
|
||||||
let flexClassName = "";
|
let flexClassName = "";
|
||||||
@ -63,7 +80,7 @@ function Input(props: InputProps) {
|
|||||||
const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash);
|
const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash);
|
||||||
|
|
||||||
if (type === InputType.checkbox) {
|
if (type === InputType.checkbox) {
|
||||||
checked = (value === String(true));
|
checked = value === String(true);
|
||||||
showValue = undefined;
|
showValue = undefined;
|
||||||
divClassName = "form-check";
|
divClassName = "form-check";
|
||||||
className = "form-check-input";
|
className = "form-check-input";
|
||||||
@ -72,11 +89,16 @@ function Input(props: InputProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === InputType.checkbox) {
|
if (type === InputType.checkbox) {
|
||||||
divClassName += ' allignedCheckBox';
|
divClassName += " allignedCheckBox";
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderType = (type === InputType.password && showPasswordIcon === faEye) ? InputType.text : type;
|
const renderType =
|
||||||
const divEyeIconClassName = (readOnly) ? "fullHeight disabledIcon" : "fullHeight";
|
type === InputType.password && showPasswordIcon === faEye
|
||||||
|
? InputType.text
|
||||||
|
: type;
|
||||||
|
const divEyeIconClassName = readOnly
|
||||||
|
? "fullHeight disabledIcon"
|
||||||
|
: "fullHeight";
|
||||||
|
|
||||||
if (type === InputType.password) {
|
if (type === InputType.password) {
|
||||||
flexClassName += "flex";
|
flexClassName += "flex";
|
||||||
@ -84,11 +106,13 @@ function Input(props: InputProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={divClassName} hidden={hidden}>
|
<div className={divClassName} hidden={hidden}>
|
||||||
{(includeLabel === true || includeLabel === undefined) && <label className={labelClassName} htmlFor={name} hidden={hidden}>
|
{(includeLabel === true || includeLabel === undefined) && (
|
||||||
|
<label className={labelClassName} htmlFor={name} hidden={hidden}>
|
||||||
{label}
|
{label}
|
||||||
</label>}
|
</label>
|
||||||
|
)}
|
||||||
<div className={flexClassName}>
|
<div className={flexClassName}>
|
||||||
{type === InputType.textarea &&
|
{type === InputType.textarea && (
|
||||||
<textarea
|
<textarea
|
||||||
id={name}
|
id={name}
|
||||||
className={className}
|
className={className}
|
||||||
@ -96,9 +120,10 @@ function Input(props: InputProps) {
|
|||||||
onChange={onTextAreaChange}
|
onChange={onTextAreaChange}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
value={showValue || defaultValue}
|
value={showValue || defaultValue}
|
||||||
autoComplete={autoComplete}>
|
autoComplete={autoComplete}
|
||||||
</textarea>}
|
></textarea>
|
||||||
{type !== InputType.textarea &&
|
)}
|
||||||
|
{type !== InputType.textarea && (
|
||||||
<input
|
<input
|
||||||
{...rest}
|
{...rest}
|
||||||
id={name}
|
id={name}
|
||||||
@ -113,13 +138,20 @@ function Input(props: InputProps) {
|
|||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
maxLength={maxLength! > 0 ? maxLength : undefined}
|
maxLength={maxLength! > 0 ? maxLength : undefined}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
/>}
|
/>
|
||||||
{type === InputType.password && <div className={divEyeIconClassName} ><FontAwesomeIcon className="passwordIcon" icon={showPasswordIcon} onClick={() => {
|
)}
|
||||||
|
{type === InputType.password && (
|
||||||
|
<div className={divEyeIconClassName}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="passwordIcon"
|
||||||
|
icon={showPasswordIcon}
|
||||||
|
onClick={() => {
|
||||||
const newIcon = showPasswordIcon === faEye ? faEyeSlash : faEye;
|
const newIcon = showPasswordIcon === faEye ? faEyeSlash : faEye;
|
||||||
setShowPasswordIcon(newIcon)
|
setShowPasswordIcon(newIcon);
|
||||||
}
|
}}
|
||||||
} />
|
/>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ErrorBlock error={error}></ErrorBlock>
|
<ErrorBlock error={error}></ErrorBlock>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,26 +2,16 @@ import React from "react";
|
|||||||
import LoadingPanel from "./LoadingPanel";
|
import LoadingPanel from "./LoadingPanel";
|
||||||
|
|
||||||
interface Loading2Props {
|
interface Loading2Props {
|
||||||
loaded : boolean
|
loaded: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Loading2State {
|
const Loading: React.FC<Loading2Props> = ({ loaded, children }) => {
|
||||||
}
|
|
||||||
|
|
||||||
class Loading extends React.Component<Loading2Props, Loading2State> {
|
|
||||||
state = { loaded : false }
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loaded, children } = this.props;
|
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return (<LoadingPanel/>)
|
return <LoadingPanel />;
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (<>{children}</>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
export default Loading;
|
export default Loading;
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { FunctionComponent } from "react";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Namespaces } from "../../i18n/i18n";
|
||||||
|
|
||||||
interface LoadingProps {
|
interface LoadingProps {}
|
||||||
}
|
|
||||||
|
|
||||||
const LoadingPanel: FunctionComponent<LoadingProps> = () => {
|
const LoadingPanel: React.FC<LoadingProps> = () => {
|
||||||
return ( <div>Loading</div> );
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
}
|
|
||||||
|
return <div>{t("Loading")}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
export default LoadingPanel;
|
export default LoadingPanel;
|
||||||
@ -1,69 +1,75 @@
|
|||||||
import React from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import Option from "./option";
|
import Option from "./option";
|
||||||
import Autocomplete from "./AutoComplete";
|
import Autocomplete from "./AutoComplete";
|
||||||
import Pill from "./Pill";
|
import Pill from "./Pill";
|
||||||
|
|
||||||
interface MultiSelectProps {
|
interface MultiSelectProps {
|
||||||
includeLabel? : boolean,
|
includeLabel?: boolean;
|
||||||
name : string,
|
name: string;
|
||||||
label : string,
|
label: string;
|
||||||
error? : string,
|
error?: string;
|
||||||
|
|
||||||
// value : unknown
|
options?: Option[];
|
||||||
options? : Option[],
|
selectedOptions: Option[];
|
||||||
selectedOptions : Option[],
|
|
||||||
// includeBlankFirstEntry? : boolean,
|
|
||||||
// onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
|
||||||
|
|
||||||
onAdd: (item: Option) => void;
|
onAdd: (item: Option) => void;
|
||||||
onDelete: (item: Option) => void;
|
onDelete: (item: Option) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MultiSelectState {
|
const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||||
|
includeLabel = true,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
options,
|
||||||
|
selectedOptions,
|
||||||
|
onAdd,
|
||||||
|
onDelete,
|
||||||
|
}) => {
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const found = options?.find((x) => x._id === id);
|
||||||
|
if (found) {
|
||||||
|
onDelete(found);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[options, onDelete],
|
||||||
|
);
|
||||||
|
|
||||||
class MultiSelect extends React.Component<MultiSelectProps, MultiSelectState> {
|
const selectedBlock = useMemo(() => {
|
||||||
|
if (selectedOptions.length === 0) return <></>;
|
||||||
|
|
||||||
handleDelete = ( id: string) => {
|
return (
|
||||||
const { options, onDelete } = this.props;
|
<>
|
||||||
|
{selectedOptions.map((x) => (
|
||||||
const foundItem : Option | undefined = options?.filter( x => x._id === id)[0];
|
<Pill
|
||||||
|
key={x._id}
|
||||||
if (foundItem)
|
pillKey={x._id}
|
||||||
onDelete(foundItem);
|
displayText={x.name}
|
||||||
}
|
readOnly={false}
|
||||||
|
onClick={handleDelete}
|
||||||
render() {
|
/>
|
||||||
const { includeLabel, name, label, error, options, selectedOptions, onAdd
|
))}
|
||||||
// value, includeBlankFirstEntry, onChange, ...rest
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let selectedBlock = <></>;
|
|
||||||
|
|
||||||
if (selectedOptions.length > 0)
|
|
||||||
{
|
|
||||||
selectedBlock = <>
|
|
||||||
{
|
|
||||||
selectedOptions.map( x =>
|
|
||||||
<Pill key={x._id} pillKey={x._id} displayText={x.name} readOnly={false} onClick={this.handleDelete} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
}
|
);
|
||||||
|
}, [selectedOptions, handleDelete]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-group multiSelect multiSelectContainer">
|
<div className="form-group multiSelect multiSelectContainer">
|
||||||
{(includeLabel===undefined || includeLabel===true) && <label htmlFor={name}>{label}</label>}
|
{includeLabel && <label htmlFor={name}>{label}</label>}
|
||||||
|
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<Autocomplete options={options} selectedOptions={selectedOptions} onSelect={onAdd}/>
|
<Autocomplete
|
||||||
|
options={options}
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
|
onSelect={onAdd}
|
||||||
|
/>
|
||||||
{selectedBlock}
|
{selectedBlock}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="alert alert-danger">{error}</div>}
|
{error && <div className="alert alert-danger">{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default MultiSelect;
|
export default MultiSelect;
|
||||||
@ -1,48 +1,39 @@
|
|||||||
import React from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import Button, { ButtonType } from "./Button";
|
import Button, { ButtonType } from "./Button";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons";
|
import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
interface ExpandoProps {
|
interface ExpandoProps {
|
||||||
name:string,
|
name: string;
|
||||||
title : JSX.Element,
|
title: JSX.Element;
|
||||||
children:JSX.Element,
|
children: JSX.Element;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExpandoState {
|
const Expando: React.FC<ExpandoProps> = ({ title, children }) => {
|
||||||
expanded : boolean;
|
const [expanded, setExpanded] = useState(false);
|
||||||
}
|
|
||||||
|
|
||||||
class Expando extends React.Component<ExpandoProps, ExpandoState> {
|
const open = useCallback(() => setExpanded(true), []);
|
||||||
state : ExpandoState = {
|
const close = useCallback(() => setExpanded(false), []);
|
||||||
expanded : false
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDownClick = () => {
|
return (
|
||||||
this.setState({expanded :true})
|
<div>
|
||||||
}
|
{!expanded && (
|
||||||
|
<Button buttonType={ButtonType.secondary} onClick={open}>
|
||||||
|
{title} <FontAwesomeIcon icon={faCaretDown} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
CloseUpClick = () => {
|
{expanded && (
|
||||||
this.setState({expanded :false})
|
<div>
|
||||||
}
|
<Button buttonType={ButtonType.secondary} onClick={close}>
|
||||||
|
{title} <FontAwesomeIcon icon={faCaretUp} />
|
||||||
render() {
|
</Button>
|
||||||
const { title, children } = this.props;
|
|
||||||
const { expanded } = this.state;
|
|
||||||
|
|
||||||
if (!expanded){
|
|
||||||
return ( <div>
|
|
||||||
<Button buttonType={ButtonType.secondary} onClick={this.DropDownClick}>{title} <FontAwesomeIcon icon={faCaretDown}/></Button>
|
|
||||||
</div> );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ( <div>
|
|
||||||
<Button buttonType={ButtonType.secondary} onClick={this.CloseUpClick}>{title} <FontAwesomeIcon icon={faCaretUp}/></Button>
|
|
||||||
{children}
|
{children}
|
||||||
</div> );
|
</div>
|
||||||
}
|
)}
|
||||||
}
|
</div>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Expando;
|
export default Expando;
|
||||||
11
src/i18n/generatedLocales.ts
Normal file
11
src/i18n/generatedLocales.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
// AUTO-GENERATED FILE — DO NOT EDIT MANUALLY
|
||||||
|
|
||||||
|
export const availableLocales = [
|
||||||
|
"en-GB",
|
||||||
|
"en-US",
|
||||||
|
"fr-CA",
|
||||||
|
"fr-FR"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type Locale = typeof availableLocales[number];
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import HttpBackend from "i18next-http-backend";
|
import HttpBackend from "i18next-http-backend";
|
||||||
import { determineInitialLanguage } from "./modules/frame/services/lanugageService";
|
import { determineInitialLocale } from "../modules/frame/services/lanugageService";
|
||||||
|
|
||||||
export const Namespaces = {
|
export const Namespaces = {
|
||||||
Common: "common",
|
Common: "common",
|
||||||
@ -15,7 +15,7 @@ i18n
|
|||||||
.use(HttpBackend) // load translations from /public/locales
|
.use(HttpBackend) // load translations from /public/locales
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
lng: determineInitialLanguage(),
|
lng: determineInitialLocale(),
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
defaultNS: "common",
|
defaultNS: "common",
|
||||||
interpolation: {
|
interpolation: {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import "./i18n";
|
import "./i18n/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";
|
||||||
|
|||||||
76
src/modules/frame/components/LanguageSelector.tsx
Normal file
76
src/modules/frame/components/LanguageSelector.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { NavDropdown } from "react-bootstrap";
|
||||||
|
import { availableLocales } from "../../../i18n/generatedLocales";
|
||||||
|
import i18n from "../../../i18n/i18n";
|
||||||
|
import profileService from "../../profile/services/profileService";
|
||||||
|
|
||||||
|
function regionToFlag(region: string) {
|
||||||
|
return region
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/./g, (char) => String.fromCodePoint(127397 + char.charCodeAt(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function flagEmoji(locale: string) {
|
||||||
|
const parts = locale.split("-");
|
||||||
|
const region = parts[1];
|
||||||
|
if (!region) return "";
|
||||||
|
return regionToFlag(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyLanguageName(language: string) {
|
||||||
|
const display = new Intl.DisplayNames([i18n.language], { type: "language" });
|
||||||
|
return display.of(language) || language;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyRegionName(region: string) {
|
||||||
|
const display = new Intl.DisplayNames([i18n.language], { type: "region" });
|
||||||
|
return display.of(region) || region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLocaleLabel(locale: string) {
|
||||||
|
const [language, region] = locale.split("-");
|
||||||
|
const langName = prettyLanguageName(language);
|
||||||
|
|
||||||
|
if (!region) return langName;
|
||||||
|
|
||||||
|
const regionName = prettyRegionName(region);
|
||||||
|
return `${langName} (${regionName})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LanguageSelectorMenuItem() {
|
||||||
|
const current = i18n.language;
|
||||||
|
|
||||||
|
const currentFlag = flagEmoji(current);
|
||||||
|
const currentLabel = formatLocaleLabel(current);
|
||||||
|
|
||||||
|
async function handleSelect(locale: string) {
|
||||||
|
i18n.changeLanguage(locale);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await profileService.patchMyProfile({
|
||||||
|
preferredLocale: locale,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update preferred locale", err);
|
||||||
|
// Optional: show toast or revert language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavDropdown align="end" title={`${currentFlag} ${currentLabel}`}>
|
||||||
|
{availableLocales.map((locale) => {
|
||||||
|
const flag = flagEmoji(locale);
|
||||||
|
const label = formatLocaleLabel(locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavDropdown.Item
|
||||||
|
key={locale}
|
||||||
|
active={locale === current}
|
||||||
|
onClick={() => handleSelect(locale)}
|
||||||
|
>
|
||||||
|
{flag} {label}
|
||||||
|
</NavDropdown.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</NavDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
import LeftMenuItem from "./LeftMenuItem";
|
import LeftMenuItem from "./LeftMenuItem";
|
||||||
import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu";
|
import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Namespaces } from "../../../i18n";
|
import { Namespaces } from "../../../i18n/i18n";
|
||||||
|
|
||||||
const LeftMenu: React.FC = () => {
|
const LeftMenu: React.FC = () => {
|
||||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
@ -60,7 +60,11 @@ const LeftMenu: React.FC = () => {
|
|||||||
<LeftMenuItem to="/" icon={faHome} label={t("Home")} />
|
<LeftMenuItem to="/" icon={faHome} label={t("Home")} />
|
||||||
|
|
||||||
{viewOrganisation && (
|
{viewOrganisation && (
|
||||||
<LeftMenuItem to="/organisations" icon={faPrint} label="e-print" />
|
<LeftMenuItem
|
||||||
|
to="/organisations"
|
||||||
|
icon={faPrint}
|
||||||
|
label={t("e-print")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewAdmin && (
|
{viewAdmin && (
|
||||||
|
|||||||
@ -3,12 +3,13 @@ import { Navbar, Nav, NavDropdown } from "react-bootstrap";
|
|||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
import Logo from "../../../img/logo";
|
import Logo from "../../../img/logo";
|
||||||
|
|
||||||
import '../../../Sass/_nav.scss';
|
import "../../../Sass/_nav.scss";
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faArrowRightFromBracket } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowRightFromBracket } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faUser } from "@fortawesome/free-solid-svg-icons";
|
import { faUser } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { getCurrentUser } from "../services/authenticationService";
|
import { getCurrentUser } from "../services/authenticationService";
|
||||||
|
import { LanguageSelectorMenuItem } from "./LanguageSelector";
|
||||||
|
|
||||||
export interface TopMenuProps {
|
export interface TopMenuProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -25,8 +26,14 @@ function TopMenu(props : TopMenuProps) {
|
|||||||
<div className="navbar-left">{props.title}</div>
|
<div className="navbar-left">{props.title}</div>
|
||||||
<div className="navbar-right">
|
<div className="navbar-right">
|
||||||
<NavDropdown align="end" title="User">
|
<NavDropdown align="end" title="User">
|
||||||
<Nav.Link href="/profile" className="ps-1"><FontAwesomeIcon icon={faUser}/> Account</Nav.Link>
|
<Nav.Link href="/profile" className="ps-1">
|
||||||
<Nav.Link href="/logout" className="ps-1"><FontAwesomeIcon icon={faArrowRightFromBracket}/> Logout {user?.name} </Nav.Link>
|
<FontAwesomeIcon icon={faUser} /> Account
|
||||||
|
</Nav.Link>
|
||||||
|
<LanguageSelectorMenuItem />
|
||||||
|
<Nav.Link href="/logout" className="ps-1">
|
||||||
|
<FontAwesomeIcon icon={faArrowRightFromBracket} /> Logout{" "}
|
||||||
|
{user?.name}{" "}
|
||||||
|
</Nav.Link>
|
||||||
</NavDropdown>
|
</NavDropdown>
|
||||||
</div>
|
</div>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|||||||
@ -5,5 +5,5 @@ export default interface JwtToken {
|
|||||||
email: string;
|
email: string;
|
||||||
domainid: bigint;
|
domainid: bigint;
|
||||||
securityPrivileges: [];
|
securityPrivileges: [];
|
||||||
language?: string;
|
preferredLocale: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,18 @@ import JwtToken from "../models/JwtToken";
|
|||||||
const apiEndpoint = "/Authentication";
|
const apiEndpoint = "/Authentication";
|
||||||
//const tokenKey = "token";
|
//const tokenKey = "token";
|
||||||
|
|
||||||
export async function login(email: string, password: string, securityCode: string, requestTfaRemoval: boolean) {
|
export async function login(
|
||||||
const loginResponse = await httpService.post(apiEndpoint + "/login", { email, password, securityCode, requestTfaRemoval });
|
email: string,
|
||||||
|
password: string,
|
||||||
|
securityCode: string,
|
||||||
|
requestTfaRemoval: boolean,
|
||||||
|
) {
|
||||||
|
const loginResponse = await httpService.post(apiEndpoint + "/login", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
securityCode,
|
||||||
|
requestTfaRemoval,
|
||||||
|
});
|
||||||
|
|
||||||
if (loginResponse?.status === 202) return 1; //TFA information needed, or TFA Removal request accepted.
|
if (loginResponse?.status === 202) return 1; //TFA information needed, or TFA Removal request accepted.
|
||||||
|
|
||||||
@ -19,8 +29,7 @@ export async function login(email: string, password: string, securityCode: strin
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout() {
|
export function logout() {}
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshToken() {
|
async function refreshToken() {
|
||||||
const currentUser = getCurrentUser();
|
const currentUser = getCurrentUser();
|
||||||
@ -28,7 +37,9 @@ async function refreshToken() {
|
|||||||
const fiveMinutesFromNow: Date = new Date(Date.now() + 5 * 60 * 1000);
|
const fiveMinutesFromNow: Date = new Date(Date.now() + 5 * 60 * 1000);
|
||||||
|
|
||||||
if (currentUser.expiry < fiveMinutesFromNow) {
|
if (currentUser.expiry < fiveMinutesFromNow) {
|
||||||
const refreshTokenRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? "/../account/refreshToken" : apiEndpoint + "/refreshToken";
|
const refreshTokenRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN
|
||||||
|
? "/../account/refreshToken"
|
||||||
|
: apiEndpoint + "/refreshToken";
|
||||||
|
|
||||||
const { status } = await httpService.get(refreshTokenRoute);
|
const { status } = await httpService.get(refreshTokenRoute);
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
@ -39,8 +50,7 @@ async function refreshToken() {
|
|||||||
|
|
||||||
export function hasToken(): boolean {
|
export function hasToken(): boolean {
|
||||||
const jwt = getJwt();
|
const jwt = getJwt();
|
||||||
if (jwt)
|
if (jwt) return true;
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -56,7 +66,7 @@ export function tokenExpired(): boolean{
|
|||||||
if (expiry > now) {
|
if (expiry > now) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +90,8 @@ export function getCurrentUser(): JwtToken | null {
|
|||||||
name: decodedToken.unique_name,
|
name: decodedToken.unique_name,
|
||||||
primarysid: decodedToken.primarysid,
|
primarysid: decodedToken.primarysid,
|
||||||
domainid: decodedToken.domainid,
|
domainid: decodedToken.domainid,
|
||||||
securityPrivileges: JSON.parse(decodedToken.securityPrivileges)
|
securityPrivileges: JSON.parse(decodedToken.securityPrivileges),
|
||||||
|
preferredLocale: decodedToken.preferredLocale,
|
||||||
};
|
};
|
||||||
|
|
||||||
return jwtToken;
|
return jwtToken;
|
||||||
@ -103,7 +114,10 @@ export async function forgotPassword(email: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function completeEmailAction(action: IEmailUserAction) {
|
async function completeEmailAction(action: IEmailUserAction) {
|
||||||
const response = await httpService.post(apiEndpoint + "/completeEmailAction", action);
|
const response = await httpService.post(
|
||||||
|
apiEndpoint + "/completeEmailAction",
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
return 1;
|
return 1;
|
||||||
@ -115,9 +129,10 @@ async function completeEmailAction(action: IEmailUserAction) {
|
|||||||
export function hasAccess(accessKey: string): boolean {
|
export function hasAccess(accessKey: string): boolean {
|
||||||
var currentUser = getCurrentUser()!;
|
var currentUser = getCurrentUser()!;
|
||||||
|
|
||||||
const result = currentUser?.securityPrivileges!.filter(x => x === accessKey);
|
const result = currentUser?.securityPrivileges!.filter(
|
||||||
if (result?.length > 0)
|
(x) => x === accessKey,
|
||||||
return true;
|
);
|
||||||
|
if (result?.length > 0) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -131,7 +146,7 @@ const authentication = {
|
|||||||
completeEmailAction,
|
completeEmailAction,
|
||||||
hasToken,
|
hasToken,
|
||||||
tokenExpired,
|
tokenExpired,
|
||||||
hasAccess
|
hasAccess,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default authentication;
|
export default authentication;
|
||||||
|
|||||||
@ -1,21 +1,29 @@
|
|||||||
import { getCurrentUser } from "./authenticationService";
|
import { getCurrentUser } from "./authenticationService";
|
||||||
|
import { availableLocales } from "../../../i18n/generatedLocales";
|
||||||
|
|
||||||
export function determineInitialLanguage() {
|
const defaultLocale = "en";
|
||||||
// 1. JWT preference
|
|
||||||
const currentUser = getCurrentUser();
|
function normalise(raw: string): string {
|
||||||
if (currentUser !== null) {
|
return raw.replace("_", "-").toLowerCase();
|
||||||
const jwtLang = currentUser.language || null;
|
|
||||||
if (jwtLang) return jwtLang;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. LocalStorage
|
export function determineInitialLocale(): string {
|
||||||
//const storedLang = localStorage.getItem("appLanguage");
|
var currentUser = getCurrentUser();
|
||||||
//if (storedLang) return storedLang;
|
if (currentUser) {
|
||||||
|
return currentUser.preferredLocale;
|
||||||
// 3. Browser
|
}
|
||||||
const browserLang = navigator.language?.split("-")[0];
|
|
||||||
if (browserLang) return browserLang;
|
const raw = navigator.language || navigator.languages?.[0] || defaultLocale;
|
||||||
|
const locale = normalise(raw); // "en-gb"
|
||||||
// 4. Default
|
const base = locale.split("-")[0]; // "en"
|
||||||
return "en";
|
|
||||||
|
// Exact match (case-insensitive)
|
||||||
|
const exact = availableLocales.find((l) => l.toLowerCase() === locale);
|
||||||
|
if (exact) return exact;
|
||||||
|
|
||||||
|
// Base match
|
||||||
|
const baseMatch = availableLocales.find((l) => l.toLowerCase() === base);
|
||||||
|
if (baseMatch) return baseMatch;
|
||||||
|
|
||||||
|
return defaultLocale;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,4 +8,5 @@ export interface ProfileDetails {
|
|||||||
password: string;
|
password: string;
|
||||||
usingTwoFactorAuthentication: boolean;
|
usingTwoFactorAuthentication: boolean;
|
||||||
twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings;
|
twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings;
|
||||||
|
preferredLocale: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export async function getMyProfile(): Promise<ProfileDetails> {
|
|||||||
password: data.password,
|
password: data.password,
|
||||||
usingTwoFactorAuthentication: data.usingTwoFactorAuthentication,
|
usingTwoFactorAuthentication: data.usingTwoFactorAuthentication,
|
||||||
twoFactorAuthenticationSettings: data.twoFactorAuthenticationSettings,
|
twoFactorAuthenticationSettings: data.twoFactorAuthenticationSettings,
|
||||||
|
preferredLocale: data.preferredLocale,
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -26,7 +27,7 @@ export async function putMyProfile(
|
|||||||
email: string,
|
email: string,
|
||||||
usingTwoFactorAuthentication: boolean,
|
usingTwoFactorAuthentication: boolean,
|
||||||
securityCode: string,
|
securityCode: string,
|
||||||
password: string
|
password: string,
|
||||||
) {
|
) {
|
||||||
return await httpService.put(apiEndpoint + "/myProfile", {
|
return await httpService.put(apiEndpoint + "/myProfile", {
|
||||||
firstName,
|
firstName,
|
||||||
@ -39,9 +40,14 @@ export async function putMyProfile(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function patchMyProfile(patch: Partial<ProfileDetails>) {
|
||||||
|
return await httpService.patch(apiEndpoint + "/myProfile", patch);
|
||||||
|
}
|
||||||
|
|
||||||
const profileService = {
|
const profileService = {
|
||||||
getMyProfile,
|
getMyProfile,
|
||||||
putMyProfile,
|
putMyProfile,
|
||||||
|
patchMyProfile,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default profileService;
|
export default profileService;
|
||||||
|
|||||||
@ -1,14 +1,21 @@
|
|||||||
import axios, { InternalAxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from "axios";
|
import axios, {
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosResponse,
|
||||||
|
} from "axios";
|
||||||
import { isValid, parseISO } from "date-fns";
|
import { isValid, parseISO } from "date-fns";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
Object.defineProperty(BigInt.prototype, "toJSON", {
|
Object.defineProperty(BigInt.prototype, "toJSON", {
|
||||||
get() {
|
get() {
|
||||||
return () => Number(this);
|
return () => Number(this);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onRequest = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
const onRequest = (
|
||||||
|
config: InternalAxiosRequestConfig,
|
||||||
|
): InternalAxiosRequestConfig => {
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,7 +46,9 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupInterceptorsTo(axiosInstance: AxiosInstance): AxiosInstance {
|
export function setupInterceptorsTo(
|
||||||
|
axiosInstance: AxiosInstance,
|
||||||
|
): AxiosInstance {
|
||||||
axiosInstance.interceptors.request.use(onRequest, onRequestError);
|
axiosInstance.interceptors.request.use(onRequest, onRequestError);
|
||||||
axiosInstance.interceptors.response.use(onResponse, onResponseError);
|
axiosInstance.interceptors.response.use(onResponse, onResponseError);
|
||||||
return axiosInstance;
|
return axiosInstance;
|
||||||
@ -57,7 +66,8 @@ export function setJwt(jwt: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Get<T>(url: string, config?: any): any {
|
export function Get<T>(url: string, config?: any): any {
|
||||||
return axios.get<T>(url, config)
|
return axios
|
||||||
|
.get<T>(url, config)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
@ -67,7 +77,8 @@ export function Get<T>(url: string, config?: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Post<T>(url: string, data?: any, config?: any): any {
|
export function Post<T>(url: string, data?: any, config?: any): any {
|
||||||
return axios.post<T>(url, data, config)
|
return axios
|
||||||
|
.post<T>(url, data, config)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
@ -77,7 +88,19 @@ export function Post<T>(url: string, data?: any, config?: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Put<T>(url: string, data?: any, config?: any): any {
|
export function Put<T>(url: string, data?: any, config?: any): any {
|
||||||
return axios.put<T>(url, data, config)
|
return axios
|
||||||
|
.put<T>(url, data, config)
|
||||||
|
.then((response) => {
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Patch<T>(url: string, data?: any, config?: any): any {
|
||||||
|
return axios
|
||||||
|
.patch<T>(url, data, config)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
@ -87,7 +110,8 @@ export function Put<T>(url: string, data?: any, config?: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Delete<T>(url: string, config?: any): any {
|
export function Delete<T>(url: string, config?: any): any {
|
||||||
return axios.delete<T>(url, config)
|
return axios
|
||||||
|
.delete<T>(url, config)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
@ -100,6 +124,7 @@ const httpService = {
|
|||||||
get: Get,
|
get: Get,
|
||||||
post: Post,
|
post: Post,
|
||||||
put: Put,
|
put: Put,
|
||||||
|
patch: Patch,
|
||||||
delete: Delete,
|
delete: Delete,
|
||||||
setJwt,
|
setJwt,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user