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",
|
||||
"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",
|
||||
"prestart": "node scripts/generate-locales.js",
|
||||
"start": "concurrently \"npm run start-react\" \"npm run watch-css\" ",
|
||||
"prebuild": "node scripts/generate-locales.js",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
@ -24,5 +24,7 @@
|
||||
"SsoManager": "Sso Manager",
|
||||
"Support": "Support",
|
||||
"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 SsoManager from "./modules/manager/ssoManager/ssoManager";
|
||||
import SsoProviderDetails from "./modules/manager/ssoManager/SsoProviderDetails";
|
||||
import { Namespaces } from "./i18n";
|
||||
import { Namespaces } from "./i18n/i18n";
|
||||
|
||||
function GetSecureRoutes() {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import Button, { ButtonType } from "./Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../i18n";
|
||||
import { Namespaces } from "../../i18n/i18n";
|
||||
|
||||
export interface ConfirmButtonProps<T> {
|
||||
delayMS?: number;
|
||||
@ -21,7 +21,7 @@ function ConfirmButton<T>({
|
||||
onClick,
|
||||
}: ConfirmButtonProps<T>) {
|
||||
const [firstClick, setFirstClick] = useState(false);
|
||||
const t = useTranslation<typeof Namespaces.Common>();
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
|
||||
const handleFirstClick = useCallback(() => {
|
||||
setFirstClick(true);
|
||||
@ -47,7 +47,7 @@ function ConfirmButton<T>({
|
||||
|
||||
{firstClick && (
|
||||
<Button buttonType={ButtonType.danger} onClick={handleSecondClick}>
|
||||
{confirmMessage ?? <>t("Are you sure?")</>}
|
||||
{confirmMessage ?? t("Are you sure?")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { faAdd } from "@fortawesome/free-solid-svg-icons";
|
||||
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 { Paginated } from "../../services/Paginated";
|
||||
import { GeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
@ -8,92 +8,104 @@ import CustomFieldPicker from "../pickers/CustomFieldPicker";
|
||||
import Column from "./columns";
|
||||
import Table from "./Table";
|
||||
import Button, { ButtonType } from "./Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../i18n/i18n";
|
||||
|
||||
export type CustomFieldEditorAdd = ( newField : CustomField) => void;
|
||||
export type CustomFieldEditorDelete = ( keyValue : any ) => void;
|
||||
export type CustomFieldEditorAdd = (newField: CustomField) => void;
|
||||
export type CustomFieldEditorDelete = (keyValue: any) => void;
|
||||
|
||||
interface CustomFieldsEditorProps {
|
||||
name: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
value: CustomField[];
|
||||
exclude : CustomField[];
|
||||
onAdd? : CustomFieldEditorAdd;
|
||||
onDelete?: CustomFieldEditorDelete
|
||||
name: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
value: CustomField[];
|
||||
exclude: CustomField[];
|
||||
onAdd?: CustomFieldEditorAdd;
|
||||
onDelete?: CustomFieldEditorDelete;
|
||||
}
|
||||
|
||||
interface CustomFieldsEditorState {
|
||||
id : GeneralIdRef | undefined,
|
||||
displayName : string | undefined
|
||||
}
|
||||
const CustomFieldsEditor: React.FC<CustomFieldsEditorProps> = ({
|
||||
value,
|
||||
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> {
|
||||
columns : Column<CustomField>[] = [
|
||||
{ key: "name", label: "Name", order: "asc" },
|
||||
];
|
||||
const columns: Column<CustomField>[] = useMemo(
|
||||
() => [{ key: "name", label: t("Name"), order: "asc" }],
|
||||
[],
|
||||
);
|
||||
|
||||
state : CustomFieldsEditorState = {
|
||||
id : undefined,
|
||||
displayName: undefined
|
||||
}
|
||||
const paginated: Paginated<CustomField> = useMemo(
|
||||
() => ({
|
||||
count: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
data: value,
|
||||
}),
|
||||
[value],
|
||||
);
|
||||
|
||||
handleAdd = () => {
|
||||
const { onAdd } = this.props;
|
||||
const handleAdd = useCallback(() => {
|
||||
if (!onAdd || !id) return;
|
||||
|
||||
if (onAdd)
|
||||
{
|
||||
const {id, displayName} = this.state;
|
||||
|
||||
if (id)
|
||||
{
|
||||
const newField: CustomField = {
|
||||
id: id.id ?? BigInt(-1),
|
||||
name: String(displayName),
|
||||
fieldType: "",
|
||||
defaultValue: "",
|
||||
minEntries: 1,
|
||||
guid : id.guid
|
||||
};
|
||||
|
||||
onAdd(newField);
|
||||
|
||||
this.setState( {
|
||||
id : undefined,
|
||||
displayName: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (name: string, value: GeneralIdRef, displayValue : string) => {
|
||||
this.setState({
|
||||
id : value,
|
||||
displayName : displayValue
|
||||
});
|
||||
const newField: CustomField = {
|
||||
id: id.id ?? BigInt(-1),
|
||||
name: String(displayName),
|
||||
fieldType: "",
|
||||
defaultValue: "",
|
||||
minEntries: 1,
|
||||
guid: id.guid,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, exclude, label, onDelete } = this.props;
|
||||
onAdd(newField);
|
||||
|
||||
const paginated : Paginated<CustomField> = {
|
||||
count: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
data: value
|
||||
}
|
||||
setId(undefined);
|
||||
setDisplayName(undefined);
|
||||
}, [onAdd, id, displayName]);
|
||||
|
||||
return <div>
|
||||
{label}
|
||||
<Table data={ paginated } keyName="id" columns={this.columns} onDelete={onDelete}/>
|
||||
<div>
|
||||
<CustomFieldPicker name="customField" label={"Add"} exclude={exclude} value={undefined} onChange={this.handleChange}/>
|
||||
<Button buttonType={ButtonType.primary} onClick={this.handleAdd} disabled={this.state.id === undefined}>
|
||||
<FontAwesomeIcon icon={faAdd}/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
const handleChange = useCallback(
|
||||
(name: string, value: GeneralIdRef, displayValue: string) => {
|
||||
setId(value);
|
||||
setDisplayName(displayValue);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label}
|
||||
<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} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomFieldsEditor;
|
||||
|
||||
@ -1,74 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import TabHeader from './TabHeader';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import TabHeader from "./TabHeader";
|
||||
|
||||
interface HorizontalTabsProps{
|
||||
children : JSX.Element[];
|
||||
interface HorizontalTabsProps {
|
||||
children: JSX.Element[];
|
||||
}
|
||||
|
||||
interface HorizontalTabsState{
|
||||
activeTab : string;
|
||||
redirect: string;
|
||||
}
|
||||
const HorizontalTabs: React.FC<HorizontalTabsProps> = ({ children }) => {
|
||||
const [activeTab, setActiveTab] = useState<string>("");
|
||||
const [redirect, setRedirect] = useState<string>("");
|
||||
|
||||
class HorizontalTabs extends React.Component<HorizontalTabsProps, HorizontalTabsState> {
|
||||
componentDidMount(): void {
|
||||
this.onClickTabItem(this.props.children[0].props.label);
|
||||
// Set initial tab on mount
|
||||
useEffect(() => {
|
||||
if (children.length > 0) {
|
||||
setActiveTab(children[0].props.label);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
onClickTabItem = (tab : string) => {
|
||||
let { activeTab } = this.state;
|
||||
const onClickTabItem = useCallback((tab: string) => {
|
||||
setActiveTab((prev) => (prev !== tab ? tab : prev));
|
||||
}, []);
|
||||
|
||||
if (activeTab !== tab) {
|
||||
activeTab = tab;
|
||||
}
|
||||
const activeTabChildren = useMemo(() => {
|
||||
const match = children.find((child) => child.props.label === activeTab);
|
||||
return match ? match.props.children : <></>;
|
||||
}, [children, activeTab]);
|
||||
|
||||
this.setState({ activeTab } );
|
||||
};
|
||||
if (redirect !== "") {
|
||||
return <Navigate to={redirect} />;
|
||||
}
|
||||
|
||||
state : HorizontalTabsState= {
|
||||
activeTab : "",
|
||||
redirect : ""
|
||||
}
|
||||
// If only one tab, just render its content
|
||||
if (children.length === 1) {
|
||||
return <>{activeTabChildren}</>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { activeTab, redirect } = this.state;
|
||||
return (
|
||||
<div className="horizionalTabs">
|
||||
<div>
|
||||
<ul className="tab-list">
|
||||
{children.map((child) => {
|
||||
const { label } = child.props;
|
||||
return (
|
||||
<TabHeader
|
||||
key={label}
|
||||
label={label}
|
||||
isActive={label === activeTab}
|
||||
onClick={onClickTabItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
if (redirect !== "") return <Navigate to={redirect} />;
|
||||
|
||||
const filteredTabs = children.filter( child => child.props.label === activeTab );
|
||||
|
||||
const activeTabChildren = (filteredTabs?.length > 0) ? filteredTabs[0].props.children : <></>
|
||||
<div>{activeTabChildren}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (children?.length === 1) {
|
||||
return (<>{activeTabChildren}</>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="horizionalTabs">
|
||||
<div>
|
||||
<ul className="tab-list">
|
||||
{children.map((child) => {
|
||||
const { label } = child.props;
|
||||
|
||||
return (
|
||||
<TabHeader
|
||||
isActive={label === activeTab}
|
||||
key={label}
|
||||
label={label}
|
||||
onClick={this.onClickTabItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
{activeTabChildren}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HorizontalTabs;
|
||||
export default HorizontalTabs;
|
||||
|
||||
@ -1,129 +1,161 @@
|
||||
import React, { useState } from "react";
|
||||
import '../../Sass/_forms.scss';
|
||||
import "../../Sass/_forms.scss";
|
||||
import ErrorBlock from "./ErrorBlock";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export enum InputType {
|
||||
button = "button",
|
||||
checkbox = "checkbox",
|
||||
color = "color",
|
||||
date = "date",
|
||||
datetimelocal = "datetime-local",
|
||||
email = "email",
|
||||
file = "file",
|
||||
hidden = "hidden",
|
||||
image = "image",
|
||||
month = "month",
|
||||
number = "number",
|
||||
password = "password",
|
||||
radio = "radio",
|
||||
range = "range",
|
||||
reset = "reset",
|
||||
search = "search",
|
||||
submit = "submit",
|
||||
tel = "tel",
|
||||
text = "text",
|
||||
textarea = "textarea",
|
||||
time = "time",
|
||||
url = "url",
|
||||
week = "week",
|
||||
button = "button",
|
||||
checkbox = "checkbox",
|
||||
color = "color",
|
||||
date = "date",
|
||||
datetimelocal = "datetime-local",
|
||||
email = "email",
|
||||
file = "file",
|
||||
hidden = "hidden",
|
||||
image = "image",
|
||||
month = "month",
|
||||
number = "number",
|
||||
password = "password",
|
||||
radio = "radio",
|
||||
range = "range",
|
||||
reset = "reset",
|
||||
search = "search",
|
||||
submit = "submit",
|
||||
tel = "tel",
|
||||
text = "text",
|
||||
textarea = "textarea",
|
||||
time = "time",
|
||||
url = "url",
|
||||
week = "week",
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
includeLabel?: boolean;
|
||||
name: string;
|
||||
label: string;
|
||||
error: string;
|
||||
placeHolder?: string;
|
||||
readOnly?: boolean;
|
||||
type: InputType;
|
||||
value?: string | number | readonly string[] | undefined;
|
||||
defaultValue?: string | number | readonly string[] | undefined;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
hidden? : boolean;
|
||||
autoComplete? : string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onTextAreaChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
maxLength?: number;
|
||||
includeLabel?: boolean;
|
||||
name: string;
|
||||
label: string;
|
||||
error: string;
|
||||
placeHolder?: string;
|
||||
readOnly?: boolean;
|
||||
type: InputType;
|
||||
value?: string | number | readonly string[] | undefined;
|
||||
defaultValue?: string | number | readonly string[] | undefined;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
hidden?: boolean;
|
||||
autoComplete?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onTextAreaChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
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 checked: boolean = false;
|
||||
let showValue = value;
|
||||
let checked: boolean = false;
|
||||
|
||||
let divClassName = "form-group"
|
||||
let labelClassName = "label";
|
||||
let className = "form-control";
|
||||
let flexClassName = "";
|
||||
let divClassName = "form-group";
|
||||
let labelClassName = "label";
|
||||
let className = "form-control";
|
||||
let flexClassName = "";
|
||||
|
||||
const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash);
|
||||
const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash);
|
||||
|
||||
if (type === InputType.checkbox) {
|
||||
checked = (value === String(true));
|
||||
showValue = undefined;
|
||||
divClassName = "form-check";
|
||||
className = "form-check-input";
|
||||
labelClassName += " form-check-label";
|
||||
flexClassName += "checkbox";
|
||||
}
|
||||
if (type === InputType.checkbox) {
|
||||
checked = value === String(true);
|
||||
showValue = undefined;
|
||||
divClassName = "form-check";
|
||||
className = "form-check-input";
|
||||
labelClassName += " form-check-label";
|
||||
flexClassName += "checkbox";
|
||||
}
|
||||
|
||||
if (type === InputType.checkbox) {
|
||||
divClassName += ' allignedCheckBox';
|
||||
}
|
||||
if (type === InputType.checkbox) {
|
||||
divClassName += " allignedCheckBox";
|
||||
}
|
||||
|
||||
const renderType = (type === InputType.password && showPasswordIcon === faEye) ? InputType.text : type;
|
||||
const divEyeIconClassName = (readOnly) ? "fullHeight disabledIcon" : "fullHeight";
|
||||
const renderType =
|
||||
type === InputType.password && showPasswordIcon === faEye
|
||||
? InputType.text
|
||||
: type;
|
||||
const divEyeIconClassName = readOnly
|
||||
? "fullHeight disabledIcon"
|
||||
: "fullHeight";
|
||||
|
||||
if (type === InputType.password) {
|
||||
flexClassName += "flex";
|
||||
}
|
||||
if (type === InputType.password) {
|
||||
flexClassName += "flex";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={divClassName} hidden={hidden}>
|
||||
{(includeLabel === true || includeLabel === undefined) && <label className={labelClassName} htmlFor={name} hidden={hidden}>
|
||||
{label}
|
||||
</label>}
|
||||
<div className={flexClassName}>
|
||||
{type === InputType.textarea &&
|
||||
<textarea
|
||||
id={name}
|
||||
className={className}
|
||||
name={name}
|
||||
onChange={onTextAreaChange}
|
||||
disabled={readOnly}
|
||||
value={showValue || defaultValue}
|
||||
autoComplete={autoComplete}>
|
||||
</textarea>}
|
||||
{type !== InputType.textarea &&
|
||||
<input
|
||||
{...rest}
|
||||
id={name}
|
||||
type={renderType}
|
||||
className={className}
|
||||
placeholder={placeHolder}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
disabled={readOnly}
|
||||
value={showValue}
|
||||
checked={checked}
|
||||
defaultValue={defaultValue}
|
||||
maxLength={maxLength! > 0 ? maxLength: undefined}
|
||||
autoComplete={autoComplete}
|
||||
/>}
|
||||
{type === InputType.password && <div className={divEyeIconClassName} ><FontAwesomeIcon className="passwordIcon" icon={showPasswordIcon} onClick={() => {
|
||||
const newIcon = showPasswordIcon === faEye ? faEyeSlash : faEye;
|
||||
setShowPasswordIcon(newIcon)
|
||||
}
|
||||
} />
|
||||
</div>}
|
||||
</div>
|
||||
<ErrorBlock error={error}></ErrorBlock>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={divClassName} hidden={hidden}>
|
||||
{(includeLabel === true || includeLabel === undefined) && (
|
||||
<label className={labelClassName} htmlFor={name} hidden={hidden}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={flexClassName}>
|
||||
{type === InputType.textarea && (
|
||||
<textarea
|
||||
id={name}
|
||||
className={className}
|
||||
name={name}
|
||||
onChange={onTextAreaChange}
|
||||
disabled={readOnly}
|
||||
value={showValue || defaultValue}
|
||||
autoComplete={autoComplete}
|
||||
></textarea>
|
||||
)}
|
||||
{type !== InputType.textarea && (
|
||||
<input
|
||||
{...rest}
|
||||
id={name}
|
||||
type={renderType}
|
||||
className={className}
|
||||
placeholder={placeHolder}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
disabled={readOnly}
|
||||
value={showValue}
|
||||
checked={checked}
|
||||
defaultValue={defaultValue}
|
||||
maxLength={maxLength! > 0 ? maxLength : undefined}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
)}
|
||||
{type === InputType.password && (
|
||||
<div className={divEyeIconClassName}>
|
||||
<FontAwesomeIcon
|
||||
className="passwordIcon"
|
||||
icon={showPasswordIcon}
|
||||
onClick={() => {
|
||||
const newIcon = showPasswordIcon === faEye ? faEyeSlash : faEye;
|
||||
setShowPasswordIcon(newIcon);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ErrorBlock error={error}></ErrorBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
||||
@ -2,26 +2,16 @@ import React from "react";
|
||||
import LoadingPanel from "./LoadingPanel";
|
||||
|
||||
interface Loading2Props {
|
||||
loaded : boolean
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Loading2State {
|
||||
loaded: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
class Loading extends React.Component<Loading2Props, Loading2State> {
|
||||
state = { loaded : false }
|
||||
const Loading: React.FC<Loading2Props> = ({ loaded, children }) => {
|
||||
if (!loaded) {
|
||||
return <LoadingPanel />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loaded, children } = this.props;
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
if (!loaded) {
|
||||
return (<LoadingPanel/>)
|
||||
}
|
||||
else {
|
||||
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 {
|
||||
}
|
||||
|
||||
const LoadingPanel: FunctionComponent<LoadingProps> = () => {
|
||||
return ( <div>Loading</div> );
|
||||
}
|
||||
|
||||
export default LoadingPanel;
|
||||
interface LoadingProps {}
|
||||
|
||||
const LoadingPanel: React.FC<LoadingProps> = () => {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
|
||||
return <div>{t("Loading")}</div>;
|
||||
};
|
||||
|
||||
export default LoadingPanel;
|
||||
|
||||
@ -1,69 +1,75 @@
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import Option from "./option";
|
||||
import Autocomplete from "./AutoComplete";
|
||||
import Pill from "./Pill";
|
||||
|
||||
interface MultiSelectProps {
|
||||
includeLabel? : boolean,
|
||||
name : string,
|
||||
label : string,
|
||||
error? : string,
|
||||
includeLabel?: boolean;
|
||||
name: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
|
||||
// value : unknown
|
||||
options? : Option[],
|
||||
selectedOptions : Option[],
|
||||
// includeBlankFirstEntry? : boolean,
|
||||
// onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
options?: Option[];
|
||||
selectedOptions: Option[];
|
||||
|
||||
onAdd: (item: Option) => void;
|
||||
onDelete: (item: Option) => void;
|
||||
onAdd: (item: Option) => void;
|
||||
onDelete: (item: Option) => void;
|
||||
}
|
||||
|
||||
interface MultiSelectState {
|
||||
|
||||
}
|
||||
|
||||
class MultiSelect extends React.Component<MultiSelectProps, MultiSelectState> {
|
||||
|
||||
handleDelete = ( id: string) => {
|
||||
const { options, onDelete } = this.props;
|
||||
|
||||
const foundItem : Option | undefined = options?.filter( x => x._id === id)[0];
|
||||
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],
|
||||
);
|
||||
|
||||
if (foundItem)
|
||||
onDelete(foundItem);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { includeLabel, name, label, error, options, selectedOptions, onAdd
|
||||
// value, includeBlankFirstEntry, onChange, ...rest
|
||||
} = this.props;
|
||||
const selectedBlock = useMemo(() => {
|
||||
if (selectedOptions.length === 0) return <></>;
|
||||
|
||||
let selectedBlock = <></>;
|
||||
return (
|
||||
<>
|
||||
{selectedOptions.map((x) => (
|
||||
<Pill
|
||||
key={x._id}
|
||||
pillKey={x._id}
|
||||
displayText={x.name}
|
||||
readOnly={false}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [selectedOptions, handleDelete]);
|
||||
|
||||
if (selectedOptions.length > 0)
|
||||
{
|
||||
selectedBlock = <>
|
||||
{
|
||||
selectedOptions.map( x =>
|
||||
<Pill key={x._id} pillKey={x._id} displayText={x.name} readOnly={false} onClick={this.handleDelete} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
return (
|
||||
<div className="form-group multiSelect multiSelectContainer">
|
||||
{includeLabel && <label htmlFor={name}>{label}</label>}
|
||||
|
||||
return (
|
||||
<div className="form-group multiSelect multiSelectContainer">
|
||||
{(includeLabel===undefined || includeLabel===true) && <label htmlFor={name}>{label}</label>}
|
||||
<div className="form-control">
|
||||
<Autocomplete
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelect={onAdd}
|
||||
/>
|
||||
{selectedBlock}
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<Autocomplete options={options} selectedOptions={selectedOptions} onSelect={onAdd}/>
|
||||
{selectedBlock}
|
||||
</div>
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MultiSelect;
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
|
||||
@ -1,48 +1,39 @@
|
||||
import React from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import Button, { ButtonType } from "./Button";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface ExpandoProps {
|
||||
name:string,
|
||||
title : JSX.Element,
|
||||
children:JSX.Element,
|
||||
error: string;
|
||||
name: string;
|
||||
title: JSX.Element;
|
||||
children: JSX.Element;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface ExpandoState {
|
||||
expanded : boolean;
|
||||
}
|
||||
|
||||
class Expando extends React.Component<ExpandoProps, ExpandoState> {
|
||||
state : ExpandoState = {
|
||||
expanded : false
|
||||
}
|
||||
|
||||
DropDownClick = () => {
|
||||
this.setState({expanded :true})
|
||||
}
|
||||
const Expando: React.FC<ExpandoProps> = ({ title, children }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
CloseUpClick = () => {
|
||||
this.setState({expanded :false})
|
||||
}
|
||||
const open = useCallback(() => setExpanded(true), []);
|
||||
const close = useCallback(() => setExpanded(false), []);
|
||||
|
||||
render() {
|
||||
const { title, children } = this.props;
|
||||
const { expanded } = this.state;
|
||||
return (
|
||||
<div>
|
||||
{!expanded && (
|
||||
<Button buttonType={ButtonType.secondary} onClick={open}>
|
||||
{title} <FontAwesomeIcon icon={faCaretDown} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
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}
|
||||
</div> );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Expando;
|
||||
{expanded && (
|
||||
<div>
|
||||
<Button buttonType={ButtonType.secondary} onClick={close}>
|
||||
{title} <FontAwesomeIcon icon={faCaretUp} />
|
||||
</Button>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 { initReactI18next } from "react-i18next";
|
||||
import HttpBackend from "i18next-http-backend";
|
||||
import { determineInitialLanguage } from "./modules/frame/services/lanugageService";
|
||||
import { determineInitialLocale } from "../modules/frame/services/lanugageService";
|
||||
|
||||
export const Namespaces = {
|
||||
Common: "common",
|
||||
@ -15,7 +15,7 @@ i18n
|
||||
.use(HttpBackend) // load translations from /public/locales
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: determineInitialLanguage(),
|
||||
lng: determineInitialLocale(),
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
interpolation: {
|
||||
@ -1,4 +1,4 @@
|
||||
import "./i18n";
|
||||
import "./i18n/i18n";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
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 LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../../i18n";
|
||||
import { Namespaces } from "../../../i18n/i18n";
|
||||
|
||||
const LeftMenu: React.FC = () => {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
@ -60,7 +60,11 @@ const LeftMenu: React.FC = () => {
|
||||
<LeftMenuItem to="/" icon={faHome} label={t("Home")} />
|
||||
|
||||
{viewOrganisation && (
|
||||
<LeftMenuItem to="/organisations" icon={faPrint} label="e-print" />
|
||||
<LeftMenuItem
|
||||
to="/organisations"
|
||||
icon={faPrint}
|
||||
label={t("e-print")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewAdmin && (
|
||||
|
||||
@ -3,34 +3,41 @@ import { Navbar, Nav, NavDropdown } from "react-bootstrap";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import Logo from "../../../img/logo";
|
||||
|
||||
import '../../../Sass/_nav.scss';
|
||||
import "../../../Sass/_nav.scss";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {faArrowRightFromBracket} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faUser} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowRightFromBracket } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getCurrentUser } from "../services/authenticationService";
|
||||
import { LanguageSelectorMenuItem } from "./LanguageSelector";
|
||||
|
||||
export interface TopMenuProps{
|
||||
title?: string;
|
||||
export interface TopMenuProps {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function TopMenu(props : TopMenuProps) {
|
||||
const user = getCurrentUser();
|
||||
function TopMenu(props: TopMenuProps) {
|
||||
const user = getCurrentUser();
|
||||
|
||||
return (
|
||||
<Navbar className="navbar bg-body-tertiary px-4 Header">
|
||||
<Navbar.Brand href="/">
|
||||
<Logo alt="esuite logo"/>
|
||||
</Navbar.Brand>
|
||||
<div className="navbar-left">{props.title}</div>
|
||||
<div className="navbar-right">
|
||||
<NavDropdown align="end" title="User">
|
||||
<Nav.Link href="/profile" className="ps-1"><FontAwesomeIcon icon={faUser}/> Account</Nav.Link>
|
||||
<Nav.Link href="/logout" className="ps-1"><FontAwesomeIcon icon={faArrowRightFromBracket}/> Logout {user?.name} </Nav.Link>
|
||||
</NavDropdown>
|
||||
</div>
|
||||
</Navbar>
|
||||
);
|
||||
return (
|
||||
<Navbar className="navbar bg-body-tertiary px-4 Header">
|
||||
<Navbar.Brand href="/">
|
||||
<Logo alt="esuite logo" />
|
||||
</Navbar.Brand>
|
||||
<div className="navbar-left">{props.title}</div>
|
||||
<div className="navbar-right">
|
||||
<NavDropdown align="end" title="User">
|
||||
<Nav.Link href="/profile" className="ps-1">
|
||||
<FontAwesomeIcon icon={faUser} /> Account
|
||||
</Nav.Link>
|
||||
<LanguageSelectorMenuItem />
|
||||
<Nav.Link href="/logout" className="ps-1">
|
||||
<FontAwesomeIcon icon={faArrowRightFromBracket} /> Logout{" "}
|
||||
{user?.name}{" "}
|
||||
</Nav.Link>
|
||||
</NavDropdown>
|
||||
</div>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopMenu;
|
||||
|
||||
@ -5,5 +5,5 @@ export default interface JwtToken {
|
||||
email: string;
|
||||
domainid: bigint;
|
||||
securityPrivileges: [];
|
||||
language?: string;
|
||||
preferredLocale: string;
|
||||
}
|
||||
|
||||
@ -7,131 +7,146 @@ import JwtToken from "../models/JwtToken";
|
||||
const apiEndpoint = "/Authentication";
|
||||
//const tokenKey = "token";
|
||||
|
||||
export async function login(email: string, password: string, securityCode: string, requestTfaRemoval: boolean) {
|
||||
const loginResponse = await httpService.post(apiEndpoint + "/login", { email, password, securityCode, requestTfaRemoval });
|
||||
export async function login(
|
||||
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.
|
||||
|
||||
if (loginResponse?.status === 200) {
|
||||
return 2;
|
||||
}
|
||||
if (loginResponse?.status === 200) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
}
|
||||
export function logout() {}
|
||||
|
||||
async function refreshToken() {
|
||||
const currentUser = getCurrentUser();
|
||||
if (currentUser) {
|
||||
const fiveMinutesFromNow: Date = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
if (currentUser.expiry < fiveMinutesFromNow) {
|
||||
const refreshTokenRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? "/../account/refreshToken" : apiEndpoint + "/refreshToken";
|
||||
|
||||
const { status } = await httpService.get(refreshTokenRoute);
|
||||
if (status === 200) {
|
||||
}
|
||||
}
|
||||
const currentUser = getCurrentUser();
|
||||
if (currentUser) {
|
||||
const fiveMinutesFromNow: Date = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
if (currentUser.expiry < fiveMinutesFromNow) {
|
||||
const refreshTokenRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN
|
||||
? "/../account/refreshToken"
|
||||
: apiEndpoint + "/refreshToken";
|
||||
|
||||
const { status } = await httpService.get(refreshTokenRoute);
|
||||
if (status === 200) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function hasToken(): boolean{
|
||||
const jwt = getJwt();
|
||||
if (jwt)
|
||||
return true;
|
||||
export function hasToken(): boolean {
|
||||
const jwt = getJwt();
|
||||
if (jwt) return true;
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tokenExpired(): boolean{
|
||||
const jwt = getJwt();
|
||||
if (jwt) {
|
||||
const decodedToken: any = jwt_decode(jwt);
|
||||
export function tokenExpired(): boolean {
|
||||
const jwt = getJwt();
|
||||
if (jwt) {
|
||||
const decodedToken: any = jwt_decode(jwt);
|
||||
|
||||
const expiry: Date = new Date(decodedToken.exp * 1000);
|
||||
const now: Date = new Date(Date.now());
|
||||
const expiry: Date = new Date(decodedToken.exp * 1000);
|
||||
const now: Date = new Date(Date.now());
|
||||
|
||||
if (expiry > now) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
if (expiry > now) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getCurrentUser(): JwtToken | null {
|
||||
try {
|
||||
const jwt = getJwt();
|
||||
if (jwt) {
|
||||
const decodedToken: any = jwt_decode(jwt);
|
||||
try {
|
||||
const jwt = getJwt();
|
||||
if (jwt) {
|
||||
const decodedToken: any = jwt_decode(jwt);
|
||||
|
||||
const expiry: Date = new Date(decodedToken.exp * 1000);
|
||||
const now: Date = new Date(Date.now());
|
||||
const expiry: Date = new Date(decodedToken.exp * 1000);
|
||||
const now: Date = new Date(Date.now());
|
||||
|
||||
if (expiry < now) {
|
||||
logout(); //The JWT Has expired, so there's no point keeping it laying around
|
||||
return null;
|
||||
}
|
||||
|
||||
const jwtToken: JwtToken = {
|
||||
expiry: expiry,
|
||||
email: decodedToken.email,
|
||||
name: decodedToken.unique_name,
|
||||
primarysid: decodedToken.primarysid,
|
||||
domainid: decodedToken.domainid,
|
||||
securityPrivileges: JSON.parse(decodedToken.securityPrivileges)
|
||||
};
|
||||
|
||||
return jwtToken;
|
||||
} else return null;
|
||||
} catch (ex) {
|
||||
if (expiry < now) {
|
||||
logout(); //The JWT Has expired, so there's no point keeping it laying around
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const jwtToken: JwtToken = {
|
||||
expiry: expiry,
|
||||
email: decodedToken.email,
|
||||
name: decodedToken.unique_name,
|
||||
primarysid: decodedToken.primarysid,
|
||||
domainid: decodedToken.domainid,
|
||||
securityPrivileges: JSON.parse(decodedToken.securityPrivileges),
|
||||
preferredLocale: decodedToken.preferredLocale,
|
||||
};
|
||||
|
||||
return jwtToken;
|
||||
} else return null;
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getJwt(): string | null {
|
||||
const eSuiteSession = Cookies.get("eSuiteSession");
|
||||
if (eSuiteSession){
|
||||
return eSuiteSession;
|
||||
}
|
||||
return null;
|
||||
const eSuiteSession = Cookies.get("eSuiteSession");
|
||||
if (eSuiteSession) {
|
||||
return eSuiteSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function forgotPassword(email: string) {
|
||||
return await httpService.post(apiEndpoint + "/forgotPassword", { email });
|
||||
return await httpService.post(apiEndpoint + "/forgotPassword", { email });
|
||||
}
|
||||
|
||||
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) {
|
||||
return 1;
|
||||
}
|
||||
if (response?.status === 200) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function hasAccess(accessKey: string): boolean {
|
||||
var currentUser = getCurrentUser()!;
|
||||
|
||||
const result = currentUser?.securityPrivileges!.filter(x => x === accessKey);
|
||||
if (result?.length > 0)
|
||||
return true;
|
||||
var currentUser = getCurrentUser()!;
|
||||
|
||||
return false;
|
||||
const result = currentUser?.securityPrivileges!.filter(
|
||||
(x) => x === accessKey,
|
||||
);
|
||||
if (result?.length > 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const authentication = {
|
||||
login,
|
||||
logout,
|
||||
getCurrentUser,
|
||||
refreshToken,
|
||||
forgotPassword,
|
||||
completeEmailAction,
|
||||
hasToken,
|
||||
tokenExpired,
|
||||
hasAccess
|
||||
login,
|
||||
logout,
|
||||
getCurrentUser,
|
||||
refreshToken,
|
||||
forgotPassword,
|
||||
completeEmailAction,
|
||||
hasToken,
|
||||
tokenExpired,
|
||||
hasAccess,
|
||||
};
|
||||
|
||||
export default authentication;
|
||||
|
||||
@ -1,21 +1,29 @@
|
||||
import { getCurrentUser } from "./authenticationService";
|
||||
import { availableLocales } from "../../../i18n/generatedLocales";
|
||||
|
||||
export function determineInitialLanguage() {
|
||||
// 1. JWT preference
|
||||
const currentUser = getCurrentUser();
|
||||
if (currentUser !== null) {
|
||||
const jwtLang = currentUser.language || null;
|
||||
if (jwtLang) return jwtLang;
|
||||
const defaultLocale = "en";
|
||||
|
||||
function normalise(raw: string): string {
|
||||
return raw.replace("_", "-").toLowerCase();
|
||||
}
|
||||
|
||||
export function determineInitialLocale(): string {
|
||||
var currentUser = getCurrentUser();
|
||||
if (currentUser) {
|
||||
return currentUser.preferredLocale;
|
||||
}
|
||||
|
||||
// 2. LocalStorage
|
||||
//const storedLang = localStorage.getItem("appLanguage");
|
||||
//if (storedLang) return storedLang;
|
||||
const raw = navigator.language || navigator.languages?.[0] || defaultLocale;
|
||||
const locale = normalise(raw); // "en-gb"
|
||||
const base = locale.split("-")[0]; // "en"
|
||||
|
||||
// 3. Browser
|
||||
const browserLang = navigator.language?.split("-")[0];
|
||||
if (browserLang) return browserLang;
|
||||
// Exact match (case-insensitive)
|
||||
const exact = availableLocales.find((l) => l.toLowerCase() === locale);
|
||||
if (exact) return exact;
|
||||
|
||||
// 4. Default
|
||||
return "en";
|
||||
// Base match
|
||||
const baseMatch = availableLocales.find((l) => l.toLowerCase() === base);
|
||||
if (baseMatch) return baseMatch;
|
||||
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { TwoFactorAuthenticationSettings } from "./TwoFactorAuthenticationSettings";
|
||||
|
||||
export interface ProfileDetails {
|
||||
firstName: string;
|
||||
middleNames: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
usingTwoFactorAuthentication: boolean;
|
||||
twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings;
|
||||
firstName: string;
|
||||
middleNames: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
usingTwoFactorAuthentication: boolean;
|
||||
twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings;
|
||||
preferredLocale: string;
|
||||
}
|
||||
|
||||
@ -4,44 +4,50 @@ import { ProfileDetails } from "../models/ProfileDetails";
|
||||
const apiEndpoint = "/Profile";
|
||||
|
||||
export async function getMyProfile(): Promise<ProfileDetails> {
|
||||
const { data } = await httpService.get(apiEndpoint + "/myProfile");
|
||||
const { data } = await httpService.get(apiEndpoint + "/myProfile");
|
||||
|
||||
const result: ProfileDetails = {
|
||||
firstName: data.firstName,
|
||||
middleNames: data.middleNames,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
usingTwoFactorAuthentication: data.usingTwoFactorAuthentication,
|
||||
twoFactorAuthenticationSettings: data.twoFactorAuthenticationSettings,
|
||||
};
|
||||
const result: ProfileDetails = {
|
||||
firstName: data.firstName,
|
||||
middleNames: data.middleNames,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
usingTwoFactorAuthentication: data.usingTwoFactorAuthentication,
|
||||
twoFactorAuthenticationSettings: data.twoFactorAuthenticationSettings,
|
||||
preferredLocale: data.preferredLocale,
|
||||
};
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function putMyProfile(
|
||||
firstName: string,
|
||||
middleNames: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
usingTwoFactorAuthentication: boolean,
|
||||
securityCode: string,
|
||||
password: string
|
||||
firstName: string,
|
||||
middleNames: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
usingTwoFactorAuthentication: boolean,
|
||||
securityCode: string,
|
||||
password: string,
|
||||
) {
|
||||
return await httpService.put(apiEndpoint + "/myProfile", {
|
||||
firstName,
|
||||
middleNames,
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
usingTwoFactorAuthentication,
|
||||
securityCode,
|
||||
});
|
||||
return await httpService.put(apiEndpoint + "/myProfile", {
|
||||
firstName,
|
||||
middleNames,
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
usingTwoFactorAuthentication,
|
||||
securityCode,
|
||||
});
|
||||
}
|
||||
|
||||
export async function patchMyProfile(patch: Partial<ProfileDetails>) {
|
||||
return await httpService.patch(apiEndpoint + "/myProfile", patch);
|
||||
}
|
||||
|
||||
const profileService = {
|
||||
getMyProfile,
|
||||
putMyProfile,
|
||||
getMyProfile,
|
||||
putMyProfile,
|
||||
patchMyProfile,
|
||||
};
|
||||
|
||||
export default profileService;
|
||||
|
||||
@ -1,107 +1,132 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from "axios";
|
||||
import axios, {
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosResponse,
|
||||
} from "axios";
|
||||
import { isValid, parseISO } from "date-fns";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
Object.defineProperty(BigInt.prototype, "toJSON", {
|
||||
get() {
|
||||
return () => Number(this);
|
||||
}
|
||||
get() {
|
||||
return () => Number(this);
|
||||
},
|
||||
});
|
||||
|
||||
const onRequest = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
||||
return config;
|
||||
const onRequest = (
|
||||
config: InternalAxiosRequestConfig,
|
||||
): InternalAxiosRequestConfig => {
|
||||
return config;
|
||||
};
|
||||
|
||||
const onRequestError = (error: AxiosError): Promise<AxiosError> => {
|
||||
return Promise.reject(error);
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
export function handleDates(body: any) {
|
||||
if (body === null || body === undefined || typeof body !== "object")
|
||||
return body;
|
||||
|
||||
for (const key of Object.keys(body)) {
|
||||
const value = body[key];
|
||||
if (value !== undefined && value !== "" && isNaN(value)) {
|
||||
const parsedValue : Date = parseISO(value);
|
||||
if (isValid(parsedValue)) body[key] = parsedValue;
|
||||
else if (typeof value === "object") handleDates(value);
|
||||
}
|
||||
if (body === null || body === undefined || typeof body !== "object")
|
||||
return body;
|
||||
|
||||
for (const key of Object.keys(body)) {
|
||||
const value = body[key];
|
||||
if (value !== undefined && value !== "" && isNaN(value)) {
|
||||
const parsedValue: Date = parseISO(value);
|
||||
if (isValid(parsedValue)) body[key] = parsedValue;
|
||||
else if (typeof value === "object") handleDates(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onResponse = (response: AxiosResponse): AxiosResponse => {
|
||||
handleDates(response.data);
|
||||
return response;
|
||||
handleDates(response.data);
|
||||
return response;
|
||||
};
|
||||
|
||||
const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
||||
return Promise.reject(error);
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
export function setupInterceptorsTo(axiosInstance: AxiosInstance): AxiosInstance {
|
||||
axiosInstance.interceptors.request.use(onRequest, onRequestError);
|
||||
axiosInstance.interceptors.response.use(onResponse, onResponseError);
|
||||
return axiosInstance;
|
||||
export function setupInterceptorsTo(
|
||||
axiosInstance: AxiosInstance,
|
||||
): AxiosInstance {
|
||||
axiosInstance.interceptors.request.use(onRequest, onRequestError);
|
||||
axiosInstance.interceptors.response.use(onResponse, onResponseError);
|
||||
return axiosInstance;
|
||||
}
|
||||
|
||||
axios.defaults.baseURL = window.__RUNTIME_CONFIG__.API_URL;
|
||||
setupInterceptorsTo(axios);
|
||||
|
||||
export function setJwt(jwt: string | null) {
|
||||
if (jwt) {
|
||||
axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
|
||||
} else {
|
||||
delete axios.defaults.headers.common["Authorization"];
|
||||
}
|
||||
if (jwt) {
|
||||
axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
|
||||
} else {
|
||||
delete axios.defaults.headers.common["Authorization"];
|
||||
}
|
||||
}
|
||||
|
||||
export function Get<T>(url: string, config?: any): any {
|
||||
return axios.get<T>(url, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
return axios
|
||||
.get<T>(url, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
export function Post<T>(url: string, data?: any, config?: any): any {
|
||||
return axios.post<T>(url, data, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
return axios
|
||||
.post<T>(url, data, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
export function Put<T>(url: string, data?: any, config?: any): any {
|
||||
return axios.put<T>(url, data, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
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) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
export function Delete<T>(url: string, config?: any): any {
|
||||
return axios.delete<T>(url, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
return axios
|
||||
.delete<T>(url, config)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
const httpService = {
|
||||
get: Get,
|
||||
post: Post,
|
||||
put: Put,
|
||||
delete: Delete,
|
||||
setJwt,
|
||||
get: Get,
|
||||
post: Post,
|
||||
put: Put,
|
||||
patch: Patch,
|
||||
delete: Delete,
|
||||
setJwt,
|
||||
};
|
||||
|
||||
export default httpService;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user